├── .circleci └── config.yml ├── .dockerignore ├── .github └── workflows │ └── ruby.yml ├── .gitignore ├── .powder ├── .rspec ├── .rubocop.yml ├── .ruby-version ├── CHANGELOG.md ├── Dockerfile ├── FUNDING.yml ├── Gemfile ├── Gemfile.lock ├── Guardfile ├── LICENSE ├── README.md ├── Rakefile ├── VERSION ├── app ├── assets │ ├── config │ │ └── manifest.js │ ├── images │ │ ├── clipboard_icon.svg │ │ ├── clock-fill.svg │ │ ├── close_icon.svg │ │ ├── default_avatar.png │ │ ├── default_avatar.svg │ │ ├── eu_flag.png │ │ ├── fb.svg │ │ ├── iaac.png │ │ ├── in.svg │ │ ├── info_icon.svg │ │ ├── location_icon.svg │ │ ├── logo_fablab_bcn_small.png │ │ ├── map-pin-experiment.svg │ │ ├── map-pin-shadow.png │ │ ├── map-pin.svg │ │ ├── sck.png │ │ ├── sck_bg.png │ │ ├── sckit_1.png │ │ ├── sckit_2.png │ │ ├── sckit_2.png~ │ │ ├── smartcitizen-2-2-kit.gif │ │ ├── smartcitizen_logo.svg │ │ ├── smartcitizen_logo2.svg │ │ ├── status_icon.svg │ │ ├── tag_icon.svg │ │ ├── tw.svg │ │ ├── uptimerobot-logo.svg │ │ ├── url_icon_light.svg │ │ └── user_details_icon_light.svg │ └── stylesheets │ │ ├── application.scss │ │ ├── bootstrap_variables.scss │ │ ├── breadcrumbs.scss │ │ ├── components │ │ ├── copyable_input.scss │ │ ├── devices_typeahead.scss │ │ ├── extra_info.scss │ │ ├── map.scss │ │ ├── map_location_picker.scss │ │ ├── profile_header.scss │ │ └── reading.scss │ │ ├── flatpickr_overrides.scss │ │ ├── fonts.scss │ │ ├── footer.scss │ │ ├── global.scss │ │ ├── listing.scss │ │ ├── nav.scss │ │ └── vendor │ │ └── flatpickr.min.css ├── controllers │ ├── application_api_controller.rb │ ├── application_controller.rb │ ├── discourse_controller.rb │ ├── shared_controller_methods.rb │ ├── ui │ │ ├── application_controller.rb │ │ ├── devices_controller.rb │ │ ├── experiments_controller.rb │ │ ├── sessions_controller.rb │ │ ├── static_controller.rb │ │ └── users_controller.rb │ ├── v0 │ │ ├── application_controller.rb │ │ ├── components_controller.rb │ │ ├── devices_controller.rb │ │ ├── discourse_controller.rb │ │ ├── errors_controller.rb │ │ ├── experiments_controller.rb │ │ ├── forwarding_controller.rb │ │ ├── me_controller.rb │ │ ├── measurements_controller.rb │ │ ├── oauth_applications_controller.rb │ │ ├── onboarding │ │ │ ├── device_registrations_controller.rb │ │ │ └── orphan_devices_controller.rb │ │ ├── password_resets_controller.rb │ │ ├── readings_controller.rb │ │ ├── sensors_controller.rb │ │ ├── sessions_controller.rb │ │ ├── static_controller.rb │ │ ├── tag_sensors_controller.rb │ │ ├── tags_controller.rb │ │ └── users_controller.rb │ └── v1 │ │ ├── application_controller.rb │ │ └── devices_controller.rb ├── grammars │ └── raw_message.tt ├── helpers │ ├── application_helper.rb │ ├── presentation_helper.rb │ └── user_helper.rb ├── javascript │ ├── application.js │ ├── components │ │ ├── copyable_input.js │ │ ├── devices_typeahead.js │ │ ├── extra_info.js │ │ ├── map.js │ │ ├── map_location_picker.js │ │ └── reading.js │ └── packs │ │ └── application.js ├── jobs │ ├── application_job.rb │ ├── check_battery_level_below_job.rb │ ├── check_device_stopped_publishing_job.rb │ ├── checkup_notify_job.rb │ ├── checkup_user_email_blank_job.rb │ ├── csv_upload_job.rb │ ├── delete_archived_devices_job.rb │ ├── delete_archived_users_job.rb │ ├── delete_orphaned_devices_job.rb │ ├── mqtt_forwarding_job.rb │ ├── retry_mqtt_message_job.rb │ └── send_to_datastore_job.rb ├── lib │ ├── breadcrumb.rb │ ├── data_csv_parser.rb │ ├── delete_response_headers.rb │ ├── error_handlers.rb │ ├── header_check.rb │ ├── mathematician.rb │ ├── mqtt_client_factory.rb │ ├── mqtt_forwarder.rb │ ├── mqtt_messages_handler.rb │ ├── presenters.rb │ ├── presenters │ │ ├── base_presenter.rb │ │ ├── component_presenter.rb │ │ ├── device_presenter.rb │ │ ├── measurement_presenter.rb │ │ ├── sensor_presenter.rb │ │ └── user_presenter.rb │ ├── pretty_json.rb │ ├── raw_mqtt_message_parser.rb │ └── single_sign_on.rb ├── mailers │ ├── application_mailer.rb │ └── user_mailer.rb ├── models │ ├── application_record.rb │ ├── component.rb │ ├── concerns │ │ ├── archive_workflow.rb │ │ ├── country_methods.rb │ │ ├── data_parser │ │ │ └── storer.rb │ │ └── message_forwarding.rb │ ├── device.rb │ ├── device_inventory.rb │ ├── devices_tag.rb │ ├── experiment.rb │ ├── ingest_error.rb │ ├── kairos.rb │ ├── measurement.rb │ ├── orphan_device.rb │ ├── postprocessing.rb │ ├── raw_storer.rb │ ├── sensor.rb │ ├── sensor_tag.rb │ ├── storer.rb │ ├── tag.rb │ ├── tag_sensor.rb │ ├── user.rb │ └── validators │ │ └── datetime_validator.rb ├── policies │ ├── admin_policy.rb │ ├── application_policy.rb │ ├── component_policy.rb │ ├── device_policy.rb │ ├── experiment_policy.rb │ ├── measurement_policy.rb │ ├── oauth_application_policy.rb │ ├── password_reset_policy.rb │ ├── sensor_policy.rb │ ├── session_policy.rb │ ├── tag_policy.rb │ ├── tag_sensor_policy.rb │ └── user_policy.rb ├── services │ └── device_archive.rb └── views │ ├── layouts │ ├── _breadcrumbs.html.erb │ ├── _doorbell_embed.html.erb │ ├── _flashes.html.erb │ ├── _footer.html.erb │ ├── _nav.html.erb │ └── application.html.erb │ ├── ui │ ├── devices │ │ ├── _actions.html.erb │ │ ├── _device.html.erb │ │ ├── _fields.html.erb │ │ ├── _meta.html.erb │ │ ├── _profile_header.html.erb │ │ ├── _select_list_item.html.erb │ │ ├── delete.html.erb │ │ ├── download.html.erb │ │ ├── edit.html.erb │ │ ├── new.html.erb │ │ ├── register.html.erb │ │ ├── show.html.erb │ │ └── upload.html.erb │ ├── experiments │ │ ├── _actions.html.erb │ │ ├── _devices_collection.html.erb │ │ ├── _experiment.html.erb │ │ ├── _fields.html.erb │ │ ├── _header.html.erb │ │ ├── _meta.html.erb │ │ ├── _page_nav.html.erb │ │ ├── delete.html.erb │ │ ├── edit.html.erb │ │ ├── new.html.erb │ │ ├── readings.html.erb │ │ └── show.html.erb │ ├── sessions │ │ ├── new.html.erb │ │ └── password_reset_landing.html.erb │ ├── shared │ │ ├── _box.html.erb │ │ ├── _container.html.erb │ │ ├── _copyable_input.html.erb │ │ ├── _danger_zone.html.erb │ │ ├── _extra_info.html.erb │ │ ├── _form_buttons.html.erb │ │ ├── _form_container.html.erb │ │ ├── _map.html.erb │ │ ├── _map_location_picker.html.erb │ │ ├── _profile_header.html.erb │ │ ├── _reading.html.erb │ │ ├── _title.html.erb │ │ └── _two_column.html.erb │ ├── static │ │ └── policy.html.erb │ └── users │ │ ├── _actions.html.erb │ │ ├── _devices_collection.html.erb │ │ ├── _experiments_collection.html.erb │ │ ├── _header.html.erb │ │ ├── _meta.html.erb │ │ ├── _no_device_cta.html.erb │ │ ├── _no_experiment_cta.html.erb │ │ ├── _page_nav.html.erb │ │ ├── _profile_image.html.erb │ │ ├── delete.html.erb │ │ ├── edit.html.erb │ │ ├── new.html.erb │ │ ├── post_delete.html.erb │ │ ├── secrets.html.erb │ │ └── show.html.erb │ ├── user_mailer │ ├── _email_css.html.erb │ ├── device_archive.html.erb │ ├── device_battery_low.html.erb │ ├── device_ingest_errors.html.erb │ ├── device_stopped_publishing.html.erb │ ├── password_reset.html.erb │ └── welcome.html.erb │ └── v0 │ ├── components │ ├── _component.jbuilder │ ├── index.jbuilder │ └── show.jbuilder │ ├── devices │ ├── _device.jbuilder │ ├── _world_map_device.jbuilder │ ├── _world_map_list.jbuilder │ ├── fresh_world_map.jbuilder │ ├── index.jbuilder │ ├── show.jbuilder │ └── world_map.jbuilder │ ├── experiments │ ├── _experiment.jbuilder │ ├── index.jbuilder │ └── show.jbuilder │ ├── measurements │ ├── _measurement.jbuilder │ ├── index.jbuilder │ └── show.jbuilder │ ├── oauth_applications │ ├── _oauth_application.jbuilder │ ├── index.jbuilder │ └── show.jbuilder │ ├── sensors │ ├── _sensor.jbuilder │ ├── index.jbuilder │ └── show.jbuilder │ ├── tag_sensors │ ├── index.jbuilder │ └── show.jbuilder │ ├── tags │ ├── _tag.jbuilder │ ├── index.jbuilder │ └── show.jbuilder │ └── users │ ├── _user.jbuilder │ ├── index.jbuilder │ └── show.jbuilder ├── babel.config.js ├── bin ├── bundle ├── rails ├── rake ├── rspec ├── setup ├── spring ├── update ├── webpack └── webpack-dev-server ├── ci.sh ├── compose.override.local.yml ├── compose.override.production.yml ├── compose.override.staging.yml ├── compose.yml ├── compose ├── app.yml ├── cassandra.yml ├── db.yml ├── grafana.yml ├── kairos.yml ├── mqtt-task-common.yml ├── mqtt-task.yml ├── mqtt.yml ├── redis.yml ├── sidekiq.yml ├── telnet-task.yml └── web.yml ├── config.ru ├── config ├── application.rb ├── banned_words.yml ├── boot.rb ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── api_cache.rb │ ├── application_controller_renderer.rb │ ├── assets.rb │ ├── backtrace_silencers.rb │ ├── content_security_policy.rb │ ├── cookies_serializer.rb │ ├── cors.rb │ ├── countries.rb │ ├── doorkeeper.rb │ ├── filter_parameter_logging.rb │ ├── force_ssl.rb │ ├── friendly_id.rb │ ├── geocoder.rb │ ├── git_info.rb │ ├── inflections.rb │ ├── json.rb │ ├── mime_types.rb │ ├── monkey_patches.rb │ ├── mysql.rb │ ├── new_framework_defaults_5_2.rb │ ├── new_framework_defaults_6_0.rb │ ├── pg.rb │ ├── premailer.rb │ ├── rack_attack.rb │ ├── ransack.rb │ ├── request_cloudflare_ip.rb │ ├── scheduler.rb │ ├── secret_token.rb │ ├── sentry.rb │ ├── string.rb │ ├── timeout.rb │ └── wrap_parameters.rb ├── locales │ ├── controllers │ │ └── en.yml │ ├── doorkeeper.en.yml │ ├── en.yml │ ├── helpers │ │ └── user │ │ │ └── en.yml │ └── views │ │ ├── devices │ │ └── en.yml │ │ ├── experiments │ │ └── en.yml │ │ ├── footer │ │ └── en.yml │ │ ├── layout │ │ └── en.yml │ │ ├── nav │ │ └── en.yml │ │ ├── sessions │ │ └── en.yml │ │ ├── shared │ │ ├── copyable_input │ │ │ └── en.yml │ │ └── danger_zone │ │ │ └── en.yml │ │ └── users │ │ └── en.yml ├── puma.rb ├── routes.rb ├── secrets.yml ├── sidekiq.yml ├── storage.yml ├── webpack │ ├── development.js │ ├── environment.js │ ├── production.js │ └── test.js └── webpacker.yml ├── db ├── data │ ├── kits.csv │ └── sensors_without_default_keys.csv ├── migrate │ ├── 20150126131930_create_users.rb │ ├── 20150126133546_create_doorkeeper_tables.rb │ ├── 20150126155743_create_devices.rb │ ├── 20150128122757_create_api_tokens.rb │ ├── 20150129141454_create_friendly_id_slugs.rb │ ├── 20150129163302_create_sensors.rb │ ├── 20150129170516_create_kits.rb │ ├── 20150129170545_add_kit_id_to_devices.rb │ ├── 20150129170854_create_components.rb │ ├── 20150130104734_add_hstore_to_devices.rb │ ├── 20150130104735_add_latest_data_to_devices.rb │ ├── 20150201170035_add_password_reset_to_users.rb │ ├── 20150202140127_add_geohash_to_devices.rb │ ├── 20150202154555_add_old_password_to_users.rb │ ├── 20150204174044_add_last_recorded_at_to_devices.rb │ ├── 20150204202307_add_slug_to_kits.rb │ ├── 20150211081350_add_fields_to_devices.rb │ ├── 20150211081539_add_fields_to_users.rb │ ├── 20150313203310_add_location_to_users.rb │ ├── 20150331102507_add_data_to_devices.rb │ ├── 20150424150102_add_url_to_users.rb │ ├── 20150424155114_add_avatar_to_users.rb │ ├── 20150428221051_create_pg_search_documents.rb │ ├── 20150514150525_add_old_data_to_devices.rb │ ├── 20150514150744_add_trigger_to_devices.rb │ ├── 20150520114511_add_owner_username_to_devices.rb │ ├── 20150602182642_create_pg_readings.rb │ ├── 20150701142525_create_measurements.rb │ ├── 20150701142607_add_measurement_id_to_sensors.rb │ ├── 20150701190639_add_role_mask_to_users.rb │ ├── 20150702152151_enable_uuid_extension.rb │ ├── 20150702152315_add_uuids_to_models.rb │ ├── 20150716090911_create_uploads.rb │ ├── 20150721114116_add_legacy_api_key_to_users.rb │ ├── 20150722141027_add_old_data_to_users.rb │ ├── 20150723151339_drop_unneeded_user_fields.rb │ ├── 20150727075855_add_uuids_to_uploads.rb │ ├── 20150727121643_add_user_id_to_uploads.rb │ ├── 20150727150738_add_key_to_uploads.rb │ ├── 20150728112029_drop_fk_restraint_on_devices.rb │ ├── 20150731101949_fix_pks.rb │ ├── 20150806215427_add_trigrams.rb │ ├── 20150806215704_add_unaccent.rb │ ├── 20150813072846_add_migration_data_to_devices.rb │ ├── 20150825103243_create_places.rb │ ├── 20150827133315_add_workflow_state_to_devices.rb │ ├── 20150907223941_create_tags.rb │ ├── 20150907232654_create_devices_tags.rb │ ├── 20150912192902_add_uuid_to_tags.rb │ ├── 20150916160713_add_equation_to_sensors.rb │ ├── 20150916163343_add_sensor_map_to_kits.rb │ ├── 20150917235839_add_equation_to_components.rb │ ├── 20150918012211_drop_equation_from_sensors.rb │ ├── 20150920212633_add_cached_device_ids_to_users.rb │ ├── 20151007152201_create_bad_readings.rb │ ├── 20151007191504_drop_pg_readings.rb │ ├── 20151029153355_add_csv_export_requested_at_to_devices.rb │ ├── 20151113111350_change_hstore_fields_to_jsonb_on_devices.rb │ ├── 20151117190908_add_message_to_bad_readings.rb │ ├── 20151117194000_add_device_id_and_mac_address_to_bad_readings.rb │ ├── 20151117194820_add_version_to_bad_readings.rb │ ├── 20151117200126_add_timestamp_to_bad_readings.rb │ ├── 20151118083900_add_backtrace_to_bad_readings.rb │ ├── 20151118143226_create_backup_readings.rb │ ├── 20151209135345_add_workflow_state_to_users.rb │ ├── 20160313185543_add_old_mac_address_to_devices.rb │ ├── 20160404101737_drop_places.rb │ ├── 20160411194100_add_owner_to_application.rb │ ├── 20160601120221_add_reverse_equation_to_components.rb │ ├── 20160607101112_add_state_to_devices.rb │ ├── 20161026171920_create_orphan_devices.rb │ ├── 20180719113808_create_devices_inventory.rb │ ├── 20180903161334_add_confidential_to_doorkeeper_application.rb │ ├── 20181011105328_drop_bad_readings.rb │ ├── 20181011105345_drop_backup_readings.rb │ ├── 20181105160320_create_sensor_tags.rb │ ├── 20181114144407_create_active_storage_tables.active_storage.rb │ ├── 20181212142627_add_hardware_info_to_devices.rb │ ├── 20190116161536_add_notifications_to_devices.rb │ ├── 20190222130041_add_device_handshake_to_orphan_devices.rb │ ├── 20190819084816_add_is_private_to_devices.rb │ ├── 20191115103215_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.active_storage.rb │ ├── 20200703144927_add_post_processing_info_to_device.rb │ ├── 20210105123052_add_is_test_to_devices.rb │ ├── 20210204124227_create_postprocessings.rb │ ├── 20230512075843_add_archived_at_to_devices.rb │ ├── 20230616151554_add_service_name_to_active_storage_blobs.active_storage.rb │ ├── 20230616151555_create_active_storage_variant_records.active_storage.rb │ ├── 20230704150532_refactor_kits.rb │ ├── 20230705095430_populate_device_archived_at_column.rb │ ├── 20230929114837_further_kits_refactor_changes.rb │ ├── 20231005153412_rename_component_location_to_bus.rb │ ├── 20231006064514_add_last_reading_at_to_components.rb │ ├── 20240228125910_remove_hardware_description_override_from_devices.rb │ ├── 20240318110256_add_world_map_indexes.rb │ ├── 20240318171656_add_component_device_sensor_index.rb │ ├── 20240423162838_add_forwarding_token_to_users.rb │ ├── 20240512132257_add_forwarding_username_to_users.rb │ ├── 20240624161155_add_data_policy_fields_to_device.rb │ ├── 20240624175242_add_extra_measurement_and_sensor_fields.rb │ ├── 20240704062854_remove_avatars_and_uploads.rb │ ├── 20240718054447_create_experiments.rb │ ├── 20240812081108_remove_active_flag_from_experiments.rb │ ├── 20241001080033_change_device_precise_location_default.rb │ ├── 20241009174732_unique_index_on_components.rb │ ├── 20241014052837_create_device_ingest_errors.rb │ ├── 20241113155952_remove_null_strings_from_measurement_units.rb │ └── 20250505081245_make_users_with_postprocessings_into_researchers.rb ├── schema.rb └── seeds.rb ├── deploy.sh ├── docs ├── 18-12-06.CSV ├── CSV_upload.md ├── adr │ ├── 0000-use-markdown-architectural-decision-records.md │ ├── 0001-minimize-kit-payload.md │ ├── 0002-private-devices.md │ ├── 0003-hide-test-devices.md │ ├── README.md │ └── current-architecture.png ├── erd.pdf ├── index.md ├── mqtt.md ├── onboarding.md └── throttling.md ├── entrypoint.sh ├── env.example ├── lib └── tasks │ ├── .keep │ ├── components.rake │ ├── devices.rake │ ├── i.rake │ ├── import.rake │ ├── mqtt_subscriber.rake │ ├── postgres.rake │ ├── sqlmigrate.rake │ ├── telnet.rake │ └── users.rake ├── log └── .keep ├── mqtt_subscriber.sh ├── package.json ├── postcss.config.js ├── public ├── examples │ ├── map.html │ ├── single_device.json │ └── sockets.html ├── favicon.ico └── robots.txt ├── release.sh ├── scripts ├── Dockerfile-kairos ├── cassandra │ ├── README.md │ ├── cassandra-rackdc.properties │ ├── cassandra-settings.service │ ├── cassandra-sysctl.conf │ ├── cassandra.service │ ├── configure_cassandra.sh │ ├── env.example │ ├── install_cassandra.sh │ └── repair_node.sh ├── conf │ └── kairosdb.properties ├── deploy.sh ├── dev-tools │ ├── get-token.sh │ ├── post-readings.js │ └── query-readings.sh ├── docker_backup_db.sh ├── docker_restore_db.sh ├── grafana │ └── agent.yaml ├── nginx-conf │ └── api.smartcitizen.me.conf ├── nginx.conf └── runkairos.sh ├── spec ├── controllers │ ├── ui │ │ ├── devices_controller_spec.rb │ │ ├── experiments_controller_spec.rb │ │ └── users_controller_spec.rb │ ├── v0 │ │ ├── devices_controller_spec.rb │ │ ├── discourse_controller_spec.rb │ │ ├── me_controller_spec.rb │ │ ├── measurements_controller_spec.rb │ │ ├── oauth_applications_controller_spec.rb │ │ ├── onboarding │ │ │ ├── device_registrations_controller_spec.rb │ │ │ └── orphan_devices_controller_spec.rb │ │ ├── readings_controller_spec.rb │ │ ├── sensors_controller_spec.rb │ │ ├── tags_controller_spec.rb │ │ └── users_controller_spec.rb │ └── v1 │ │ └── devices_controller_spec.rb ├── factories │ ├── backup_readings.rb │ ├── bad_readings.rb │ ├── components.rb │ ├── devices.rb │ ├── devices_inventory.rb │ ├── devices_tags.rb │ ├── doorkeeper.rb │ ├── experiment.rb │ ├── measurements.rb │ ├── orphan_devices.rb │ ├── sensors.rb │ ├── tag_sensors.rb │ ├── tags.rb │ └── users.rb ├── features │ ├── device_management_spec.rb │ ├── experiment_management_spec.rb │ ├── session_management_spec.rb │ └── user_management_spec.rb ├── fixtures │ └── fake_device_data.csv ├── jobs │ ├── check_battery_level_below_job_spec.rb │ ├── check_device_stopped_publishing_job_spec.rb │ ├── checkup_notify_job_spec.rb │ ├── checkup_user_email_blank_job_spec.rb │ ├── csv_upload_job_spec.rb │ ├── delete_archived_devices_job_spec.rb │ ├── delete_archived_users_job_spec.rb │ ├── delete_orphaned_devices_job_spec.rb │ ├── mqtt_forwarding_job_spec.rb │ ├── retry_mqtt_message_job_spec.rb │ └── send_to_datastore_job_spec.rb ├── lib │ ├── data_csv_parser_spec.rb │ ├── mqtt_forwarder_spec.rb │ ├── mqtt_messages_handler_spec.rb │ └── raw_mqtt_message_parser_spec.rb ├── mailers │ ├── previews │ │ └── user_mailer_preview.rb │ └── user_mailer_spec.rb ├── models │ ├── component_spec.rb │ ├── device_spec.rb │ ├── devices_tag_spec.rb │ ├── experiment_spec.rb │ ├── measurement_spec.rb │ ├── orphan_device_spec.rb │ ├── raw_storer_spec.rb │ ├── sensor_spec.rb │ ├── sensor_tag_spec.rb │ ├── storer_spec.rb │ ├── tag_spec.rb │ └── user_spec.rb ├── policies │ ├── application_policy_spec.rb │ ├── component_policy_spec.rb │ ├── device_policy_spec.rb │ ├── measurement_policy_spec.rb │ ├── oauth_application_policy_spec.rb │ ├── password_reset_policy_spec.rb │ ├── reading_policy_spec.rb │ ├── sensor_policy_spec.rb │ ├── tag_policy_spec.rb │ └── user_policy_spec.rb ├── presenters │ ├── component_presenter_spec.rb │ ├── device_presenter_spec.rb │ ├── measurement_presenter_spec.rb │ ├── sensor_presenter_spec.rb │ └── user_presenter_spec.rb ├── rails_helper.rb ├── requests │ ├── v0 │ │ ├── application_spec.rb │ │ ├── components_spec.rb │ │ ├── devices_spec.rb │ │ ├── errors_spec.rb │ │ ├── experiments_spec.rb │ │ ├── forwarding_spec.rb │ │ ├── me_spec.rb │ │ ├── measurements_spec.rb │ │ ├── oauth_applications_spec.rb │ │ ├── onboarding │ │ │ ├── device_registrations_spec.rb │ │ │ └── orphan_devices_spec.rb │ │ ├── password_resets_spec.rb │ │ ├── rack_attack_spec.rb │ │ ├── readings_spec.rb │ │ ├── sensors_spec.rb │ │ ├── sessions_spec.rb │ │ ├── static_spec.rb │ │ ├── tags_spec.rb │ │ └── users_spec.rb │ └── v1 │ │ └── devices_spec.rb ├── services │ └── device_archive_spec.rb ├── spec_helper.rb ├── support │ ├── api_macros.rb │ ├── env_vars.rb │ ├── mailer_macros.rb │ └── pundit_macros.rb └── vcr_cassettes │ ├── device │ └── geocoding_calculates_elevation_on_save.yml │ └── v0 │ └── readings_controller │ └── csv_archive_sends_email_to_authenticated_owner_of_kit.yml ├── tmp └── grafana_wal_data │ └── .keep └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | capybara-*.html 3 | /log/*.log* 4 | coverage 5 | tmp/cache 6 | sck-cassandra 7 | sck-cassandra-old 8 | sck-cassandra-old* 9 | -------------------------------------------------------------------------------- /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | name: Ruby 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | name: Build and test 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - name: Copy .env file 12 | uses: canastro/copy-file-action@master 13 | with: 14 | source: "env.example" 15 | target: ".env" 16 | 17 | - name: Build the Stack 18 | run: "docker compose build" 19 | 20 | - name: Start dependencies 21 | run: "docker compose up -d --no-deps db redis app" 22 | 23 | - name: Run all tests 24 | run: "docker compose exec app ./ci.sh" 25 | 26 | - name: Teardown the stack 27 | run: "docker compose down" 28 | 29 | -------------------------------------------------------------------------------- /.powder: -------------------------------------------------------------------------------- 1 | sc 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | --format=documentation -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.0.6 2 | -------------------------------------------------------------------------------- /FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: fablabbcn # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.1.5 2 | -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link application.css 3 | -------------------------------------------------------------------------------- /app/assets/images/clipboard_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | Copy to clipboard 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/assets/images/clock-fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/assets/images/close_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/assets/images/default_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fablabbcn/smartcitizen-api/cfd1394f9efd0bf49cba3b30b40b4e326d32bc7b/app/assets/images/default_avatar.png -------------------------------------------------------------------------------- /app/assets/images/eu_flag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fablabbcn/smartcitizen-api/cfd1394f9efd0bf49cba3b30b40b4e326d32bc7b/app/assets/images/eu_flag.png -------------------------------------------------------------------------------- /app/assets/images/fb.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/assets/images/iaac.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fablabbcn/smartcitizen-api/cfd1394f9efd0bf49cba3b30b40b4e326d32bc7b/app/assets/images/iaac.png -------------------------------------------------------------------------------- /app/assets/images/logo_fablab_bcn_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fablabbcn/smartcitizen-api/cfd1394f9efd0bf49cba3b30b40b4e326d32bc7b/app/assets/images/logo_fablab_bcn_small.png -------------------------------------------------------------------------------- /app/assets/images/map-pin-experiment.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/assets/images/map-pin-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fablabbcn/smartcitizen-api/cfd1394f9efd0bf49cba3b30b40b4e326d32bc7b/app/assets/images/map-pin-shadow.png -------------------------------------------------------------------------------- /app/assets/images/map-pin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/assets/images/sck.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fablabbcn/smartcitizen-api/cfd1394f9efd0bf49cba3b30b40b4e326d32bc7b/app/assets/images/sck.png -------------------------------------------------------------------------------- /app/assets/images/sck_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fablabbcn/smartcitizen-api/cfd1394f9efd0bf49cba3b30b40b4e326d32bc7b/app/assets/images/sck_bg.png -------------------------------------------------------------------------------- /app/assets/images/sckit_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fablabbcn/smartcitizen-api/cfd1394f9efd0bf49cba3b30b40b4e326d32bc7b/app/assets/images/sckit_1.png -------------------------------------------------------------------------------- /app/assets/images/sckit_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fablabbcn/smartcitizen-api/cfd1394f9efd0bf49cba3b30b40b4e326d32bc7b/app/assets/images/sckit_2.png -------------------------------------------------------------------------------- /app/assets/images/sckit_2.png~: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fablabbcn/smartcitizen-api/cfd1394f9efd0bf49cba3b30b40b4e326d32bc7b/app/assets/images/sckit_2.png~ -------------------------------------------------------------------------------- /app/assets/images/smartcitizen-2-2-kit.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fablabbcn/smartcitizen-api/cfd1394f9efd0bf49cba3b30b40b4e326d32bc7b/app/assets/images/smartcitizen-2-2-kit.gif -------------------------------------------------------------------------------- /app/assets/images/tag_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/breadcrumbs.scss: -------------------------------------------------------------------------------- 1 | .breadcrumbs { 2 | font-family: "Kanit"; 3 | letter-spacing: 0.0125rem; 4 | color: $white; 5 | a { 6 | color: $white; 7 | &:hover { 8 | color: $yellow; 9 | text-decoration: none; 10 | } 11 | } 12 | 13 | .active a { 14 | text-decoration: none; 15 | &:hover { 16 | color: $white; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/assets/stylesheets/components/copyable_input.scss: -------------------------------------------------------------------------------- 1 | .copyable-input { 2 | input { 3 | color: $black; 4 | font-family: $font-family-monospace; 5 | &:active, &:focus, &:focus-visible { 6 | outline-offset: -5px; 7 | border-width: 2px; 8 | } 9 | } 10 | svg { 11 | vertical-align: top; 12 | height: 1rem; 13 | margin-top: 3px; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/assets/stylesheets/components/extra_info.scss: -------------------------------------------------------------------------------- 1 | .extra-info { 2 | position: relative; 3 | .info { 4 | position: absolute; 5 | width: 80vw; 6 | @include media-breakpoint-up(md) { 7 | width: 50vw; 8 | } 9 | display: block; 10 | background-color: $white; 11 | text-transform: initial; 12 | font-size: var(--bs-body-font-size); 13 | letter-spacing: initial; 14 | margin-bottom:initial; 15 | font-family: var(--bs-body-font-family); 16 | font-weight: var(--bs-body-font-weight); 17 | line-height: var(--bs-body-line-height); 18 | color: var(--bs-body-color); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/assets/stylesheets/components/map.scss: -------------------------------------------------------------------------------- 1 | .map { 2 | width: 100%; 3 | height: 100%; 4 | aspect-ratio: 1; 5 | cursor: default; 6 | } 7 | 8 | -------------------------------------------------------------------------------- /app/assets/stylesheets/components/map_location_picker.scss: -------------------------------------------------------------------------------- 1 | .map-location-picker { 2 | width: 100%; 3 | aspect-ratio: 1.66; 4 | } 5 | -------------------------------------------------------------------------------- /app/assets/stylesheets/components/profile_header.scss: -------------------------------------------------------------------------------- 1 | .profile-header { 2 | background-color: $grey-900; 3 | a { 4 | color: $white !important; 5 | text-decoration: underline; 6 | &:hover { 7 | text-decoration: none; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/assets/stylesheets/fonts.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Roboto+Condensed:600,400,300,300italic,400italic,700,700italic|Roboto:400,700,700italic,400italic'); 2 | @import url('https://fonts.googleapis.com/css?family=Kanit:400,500,600,700,900'); 3 | -------------------------------------------------------------------------------- /app/assets/stylesheets/footer.scss: -------------------------------------------------------------------------------- 1 | footer { 2 | width: 100%; 3 | a { 4 | color: $primary; 5 | &:hover { 6 | text-decoration: none; 7 | } 8 | } 9 | 10 | .outlined-box { 11 | border: 2px solid $white; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/assets/stylesheets/listing.scss: -------------------------------------------------------------------------------- 1 | .listing { 2 | position: relative; 3 | 4 | .listing-link { 5 | position: absolute; 6 | top: 0; 7 | left: 0; 8 | width: 100%; 9 | height: 100%; 10 | box-sizing: border-box; 11 | } 12 | 13 | .leaflet-container { 14 | border-top: 1px solid $white; 15 | border-bottom: 1px solid $white; 16 | } 17 | 18 | &:has(:hover) { 19 | .map { 20 | background-color: $yellow; 21 | border-color: $yellow; 22 | &.experiment { 23 | background-color: $teal; 24 | border-color: $teal; 25 | } 26 | 27 | .leaflet-pane { 28 | mix-blend-mode: multiply; 29 | } 30 | 31 | .leaflet-marker-icon { 32 | filter: brightness(0) saturate(100%); 33 | } 34 | } 35 | 36 | h3 { 37 | text-decoration: underline; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | # this file is required for errbit notifier 2 | class ApplicationController < ActionController::Base 3 | end 4 | -------------------------------------------------------------------------------- /app/controllers/ui/application_controller.rb: -------------------------------------------------------------------------------- 1 | module Ui 2 | class ApplicationController < ActionController::Base 3 | layout "application" 4 | include SharedControllerMethods 5 | 6 | private 7 | 8 | def add_breadcrumbs(*crumbs) 9 | crumbs.each do |crumb| 10 | add_breadcrumb(*crumb) 11 | end 12 | end 13 | 14 | def add_breadcrumb(label, url=nil) 15 | breadcrumbs << Breadcrumb.new(breadcrumbs, label, url) 16 | end 17 | 18 | def breadcrumbs 19 | @breadcrumbs ||= [] 20 | end 21 | 22 | def goto_or(url) 23 | params[:goto].present? ? params[:goto] : url 24 | end 25 | 26 | helper_method :breadcrumbs 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/controllers/ui/static_controller.rb: -------------------------------------------------------------------------------- 1 | module Ui 2 | class StaticController < ApplicationController 3 | def policy 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/controllers/v0/application_controller.rb: -------------------------------------------------------------------------------- 1 | module V0 2 | class ApplicationController < ::ApplicationAPIController 3 | before_action :prepend_view_paths 4 | 5 | private 6 | 7 | def prepend_view_paths 8 | # is this still necessary? 9 | prepend_view_path "app/views/v0" 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/controllers/v0/components_controller.rb: -------------------------------------------------------------------------------- 1 | module V0 2 | class ComponentsController < ApplicationController 3 | 4 | def index 5 | @components = Component.all 6 | @components = paginate(@components) 7 | end 8 | 9 | def show 10 | @component = Component.includes(:device, :sensor).find(params[:id]) 11 | authorize @component 12 | end 13 | 14 | private 15 | 16 | def component_params 17 | params.permit( 18 | :device_id, 19 | :sensor_id 20 | ) 21 | end 22 | 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/controllers/v0/discourse_controller.rb: -------------------------------------------------------------------------------- 1 | module V0 2 | class DiscourseController < ApplicationController 3 | 4 | before_action :check_if_authorized! 5 | 6 | def sso 7 | secret = ENV['discourse_sso_secret'] 8 | sso = SingleSignOn.parse(request.query_string, secret) 9 | sso.email = current_user.email # from devise 10 | sso.name = current_user.full_name # this is a custom method on the User class 11 | sso.username = current_user.email # from devise 12 | #sso.username = current_user.username 13 | sso.external_id = current_user.id # from devise 14 | sso.sso_secret = secret 15 | 16 | redirect_to sso.to_url("#{ENV['discourse_endpoint']}session/sso_login") 17 | rescue => e 18 | Rails.logger.error(e.message) 19 | Rails.logger.error(e.backtrace) 20 | #flash[:error] = 'SSO error' 21 | render inline: "Error, check logs" 22 | 23 | #redirect_to "/" 24 | #redirect_to root 25 | end 26 | 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/controllers/v0/errors_controller.rb: -------------------------------------------------------------------------------- 1 | module V0 2 | class ErrorsController < ApplicationController 3 | 4 | skip_after_action :verify_authorized 5 | 6 | def not_found 7 | render json: { 8 | id: "not_found", 9 | message: "Endpoint not found", 10 | errors: nil, 11 | url: nil 12 | }, status: :not_found 13 | end 14 | 15 | def exception 16 | render json: { 17 | id: "internal_server_error", 18 | message: "Internal Server Error", 19 | errors: nil, 20 | url: nil 21 | }, status: 500 22 | end 23 | 24 | def test_error 25 | raise "test error" 26 | end 27 | 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/controllers/v0/forwarding_controller.rb: -------------------------------------------------------------------------------- 1 | module V0 2 | class ForwardingController < ApplicationController 3 | after_action :verify_authorized, except: :authorize 4 | def authorize 5 | topic = params[:topic] 6 | username = params[:username] 7 | token = topic && get_forwarding_token(topic) 8 | authorized = token && username && User.forwarding_subscription_authorized?(token, username) 9 | render json: { result: authorized ? "allow" : "deny" } 10 | end 11 | 12 | private 13 | 14 | def get_forwarding_token(topic) 15 | match = topic.match(/forward\/([^\/]+)\//) 16 | match && match[1] 17 | end 18 | end 19 | end 20 | 21 | -------------------------------------------------------------------------------- /app/controllers/v0/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | module V0 2 | class SessionsController < ApplicationController 3 | 4 | def create 5 | check_missing_params("username", "password") 6 | # user = User.find_by_username!(params[:username]) 7 | user = User.where("lower(username) = lower(?) OR lower(email) = lower(?)", params[:username], params[:username]).first! 8 | authorize user, :show? 9 | if user && user.authenticate_with_legacy_support(params[:password]) 10 | # $analytics.track("login:successful", user.id) 11 | session[:user_id] = user.id 12 | render json: { access_token: user.access_token!.token }, status: :ok 13 | else 14 | session[:user_id] = nil 15 | raise Smartcitizen::UnprocessableEntity.new({ 16 | message: {password: 'is incorrect'}, # to be removed 17 | password: 'is incorrect' # to replace the above 18 | }) 19 | end 20 | end 21 | 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/controllers/v0/tag_sensors_controller.rb: -------------------------------------------------------------------------------- 1 | module V0 2 | class TagSensorsController < ApplicationController 3 | 4 | def index 5 | @tags = TagSensor.all 6 | @tags = paginate @tags 7 | end 8 | 9 | def show 10 | @tag = TagSensor.find(params[:id]) 11 | authorize @tag 12 | end 13 | 14 | def create 15 | @tag = TagSensor.new(tag_params) 16 | authorize @tag 17 | if @tag.save 18 | render :show, status: :created 19 | else 20 | raise Smartcitizen::UnprocessableEntity.new @tag.errors 21 | end 22 | end 23 | 24 | private 25 | 26 | def tag_params 27 | params.permit( 28 | :name, 29 | :description 30 | ) 31 | end 32 | 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/controllers/v1/application_controller.rb: -------------------------------------------------------------------------------- 1 | module V1 2 | class ApplicationController < ::ApplicationAPIController 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | def show_svg(path) 3 | File.open("app/assets/images/#{path}", "rb") do |file| 4 | raw file.read 5 | end 6 | end 7 | def flash_class(level) 8 | case level.to_sym 9 | when :success then "alert alert-success" 10 | when :error then "alert alert-danger" 11 | when :alert then "alert alert-danger" 12 | else "alert alert-primary" 13 | end 14 | end 15 | 16 | 17 | def sc_nav_button_to(legend, path, opts={}) 18 | button_class = opts[:dark_buttons] ? "btn-dark" : "btn-secondary" 19 | button_class << (" " + opts[:class]) if opts[:class] 20 | button_class << " btn-active" if current_page?(path) 21 | legend << " " if opts[:external] 22 | link_to(legend.html_safe, path, class: "btn #{button_class} me-md-2 mb-3 w-100 w-md-auto", target: opts[:external] ? "_blank" : nil) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/helpers/presentation_helper.rb: -------------------------------------------------------------------------------- 1 | module PresentationHelper 2 | def present(model, options={}) 3 | Presenters.present(model, current_user, self, options) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/helpers/user_helper.rb: -------------------------------------------------------------------------------- 1 | module UserHelper 2 | def profile_picture_url(user) 3 | if user.profile_picture.attached? 4 | polymorphic_url(user.profile_picture, only_path: false) 5 | else 6 | '' 7 | end 8 | end 9 | 10 | def possessive(user, current_user, params={}) 11 | third_person = params[:third_person] 12 | first_person = params[:first_person] 13 | capitalize = params[:capitalize] 14 | if !third_person && current_user && current_user == user 15 | pronoun = t(first_person ? :first_person_possessive : :second_person_possessive) 16 | capitalize ? pronoun.capitalize : pronoun 17 | else 18 | t :third_person_possessive, username: user.username 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/javascript/application.js: -------------------------------------------------------------------------------- 1 | import * as $ from "jquery"; 2 | import flatpickr from "flatpickr"; 3 | import Tags from "bootstrap5-tags"; 4 | import {setupCopyableInputs} from "components/copyable_input"; 5 | import {setupMaps} from "components/map"; 6 | import {setupMapLocationPickers} from "components/map_location_picker"; 7 | import {setupReadings} from "components/reading"; 8 | import {setupDevicesTypeahead} from "components/devices_typeahead"; 9 | import {setupExtraInfos} from "components/extra_info"; 10 | 11 | export default function setupApplication() { 12 | $(function() { 13 | 14 | Tags.init(".tag-select", { 15 | baseClass: "tags-badge badge bg-light border text-dark text-truncate p-2 rounded-4" 16 | }); 17 | flatpickr(".flatpickr", { enableTime: true, time_24hr: true, defaultHour: 0, dateFormat: "Z", altInput: true}); 18 | setupCopyableInputs(); 19 | setupMaps(); 20 | setupMapLocationPickers(); 21 | setupExtraInfos(); 22 | setupDevicesTypeahead(); 23 | setupReadings(); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /app/javascript/components/copyable_input.js: -------------------------------------------------------------------------------- 1 | import * as $ from "jquery"; 2 | 3 | export function setupCopyableInputs() { 4 | $(".copyable-input").each(function(ix, element) { 5 | let input = $(element).find("input"); 6 | let button = $(element).find("button"); 7 | input.focus(function(event) { 8 | input.select(); 9 | }); 10 | button.click(function(event) { 11 | input.select(); 12 | navigator.clipboard.writeText(input.val()); 13 | }); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /app/javascript/components/extra_info.js: -------------------------------------------------------------------------------- 1 | import * as $ from "jquery"; 2 | 3 | export function setupExtraInfos() { 4 | $(".extra-info").each(function(ix, element) { 5 | $(element).find(".icon").on("mouseenter", function(event) { 6 | $(".extra-info .info").hide(); 7 | $(element).find(".info").show(); 8 | }); 9 | $(element).find(".icon").on("mouseout", function(event) { 10 | $(element).find(".info").hide(); 11 | }); 12 | $(element).find(".icon").on("touchstart", function(event) { 13 | const infoElement = $(element).find(".info") 14 | const initiallyHidden = $(infoElement).is(":hidden") 15 | $(".extra-info .info").hide(); 16 | if (initiallyHidden) { 17 | infoElement.show(); 18 | } 19 | }); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | # Automatically retry jobs that encountered a deadlock 3 | # retry_on ActiveRecord::Deadlocked 4 | 5 | # Most jobs are safe to ignore if the underlying records are no longer available 6 | # discard_on ActiveJob::DeserializationError 7 | end 8 | -------------------------------------------------------------------------------- /app/jobs/check_device_stopped_publishing_job.rb: -------------------------------------------------------------------------------- 1 | class CheckDeviceStoppedPublishingJob < ApplicationJob 2 | queue_as :default 3 | 4 | def perform(*args) 5 | # Do something later 6 | 7 | devices = Device.where(notify_stopped_publishing: true).where("last_reading_at < ?", 60.minutes.ago) 8 | CheckupNotifyJob.perform_now("#{devices.count} devices with notification on: stopped_publishing at least an hour ago. Ids: #{devices.pluck(:id)}") 9 | 10 | devices.each do |device| 11 | if device.notify_stopped_publishing_timestamp < 24.hours.ago 12 | device.update_column(:notify_stopped_publishing_timestamp, Time.now) 13 | UserMailer.device_stopped_publishing(device.id).deliver_now 14 | end 15 | end 16 | 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/jobs/checkup_notify_job.rb: -------------------------------------------------------------------------------- 1 | class CheckupNotifyJob < ApplicationJob 2 | queue_as :default 3 | 4 | def perform(msg) 5 | checkups_log = Logger.new('log/checkups.log', 2, 10.megabytes) 6 | checkups_log.info(msg) 7 | # TODO: send us warnings on email / slack / grafana? 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/jobs/checkup_user_email_blank_job.rb: -------------------------------------------------------------------------------- 1 | class CheckupUserEmailBlankJob < ApplicationJob 2 | queue_as :default 3 | 4 | def perform(*args) 5 | 6 | CheckupNotifyJob.perform_now("Check for blank emails ...") 7 | 8 | users = User.where(email: nil) 9 | CheckupNotifyJob.perform_now("No email for #{users.count} users. - ids: #{users.pluck(:id)}") 10 | 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/jobs/csv_upload_job.rb: -------------------------------------------------------------------------------- 1 | class CSVUploadJob < ApplicationJob 2 | queue_as :default 3 | 4 | def perform(device_id, csv_data) 5 | parsed = parser.parse(csv_data) 6 | SendToDatastoreJob.perform_now(parsed.to_json, device_id) 7 | end 8 | 9 | def parser 10 | @parser ||= DataCSVParser.new 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/jobs/delete_archived_devices_job.rb: -------------------------------------------------------------------------------- 1 | class DeleteArchivedDevicesJob < ApplicationJob 2 | queue_as :default 3 | 4 | def perform(*args) 5 | # Device.all will only look in non-archived devices because of scope 6 | CheckupNotifyJob.perform_now("Delete archived devices") 7 | 8 | Device.unscoped.where(workflow_state: "archived").each do |device| 9 | p [device.id, device.archived_at] 10 | if device.archived_at && device.archived_at < 24.hours.ago 11 | CheckupNotifyJob.perform_now("deleting archived device #{device.id}") 12 | device.destroy! 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/jobs/delete_archived_users_job.rb: -------------------------------------------------------------------------------- 1 | class DeleteArchivedUsersJob < ApplicationJob 2 | queue_as :default 3 | 4 | def perform(*args) 5 | # Do something later 6 | CheckupNotifyJob.perform_now("Delete archived users") 7 | 8 | User.unscoped.where(workflow_state: "archived").each do |user| 9 | if user.created_at < 72.hours.ago 10 | CheckupNotifyJob.perform_now("deleting archived user #{user.id}") 11 | user.destroy! 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/jobs/delete_orphaned_devices_job.rb: -------------------------------------------------------------------------------- 1 | class DeleteOrphanedDevicesJob < ApplicationJob 2 | queue_as :default 3 | 4 | def perform(*args) 5 | CheckupNotifyJob.perform_now("Delete old orphan devices") 6 | 7 | OrphanDevice.all.each do |device| 8 | if device.updated_at < 7.days.ago 9 | CheckupNotifyJob.perform_now("deleting old orphan device #{device.id}") 10 | device.destroy! 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/jobs/mqtt_forwarding_job.rb: -------------------------------------------------------------------------------- 1 | class MQTTForwardingJob < ApplicationJob 2 | 3 | queue_as :mqtt_forward 4 | 5 | def perform(device_id, data) 6 | readings = data[:readings] 7 | device = Device.find(device_id) 8 | begin 9 | forwarder = MQTTForwarder.new(mqtt_client) 10 | payload = payload_for(device, readings) 11 | forwarder.forward_readings(device.forwarding_token, device.id, payload) 12 | ensure 13 | disconnect_mqtt! 14 | end 15 | end 16 | 17 | private 18 | 19 | def payload_for(device, readings) 20 | Presenters.present(device, device.owner, nil, readings: readings).to_json 21 | end 22 | 23 | def mqtt_client 24 | @mqtt_client ||= MQTTClientFactory.create_client({ 25 | clean_session: true, client_id: nil 26 | }) 27 | end 28 | 29 | def disconnect_mqtt! 30 | @mqtt_client&.disconnect 31 | end 32 | end 33 | 34 | -------------------------------------------------------------------------------- /app/jobs/retry_mqtt_message_job.rb: -------------------------------------------------------------------------------- 1 | class RetryMQTTMessageJob < ApplicationJob 2 | class RetryMessageHandlerError < RuntimeError 3 | end 4 | 5 | queue_as :mqtt_retry 6 | 7 | 8 | retry_on(RetryMessageHandlerError, attempts: 75, wait: ->(count) { 9 | case count 10 | when 0..12 11 | 5.seconds 12 | when 12..20 # Every 30 seconds for the first 5 minutes 13 | 30.seconds 14 | else # Then every minute for an hour 15 | 1.minute 16 | end 17 | }) do |_job, _exeception| 18 | # No-op, this block ensures the exception isn't reraised and retried by Sidekiq 19 | end 20 | 21 | def perform(topic, message) 22 | result = handler.handle_topic(topic, message, false) 23 | raise RetryMessageHandlerError if result.nil? 24 | end 25 | 26 | private 27 | 28 | def handler 29 | @handler ||= MqttMessagesHandler.new 30 | end 31 | end 32 | 33 | -------------------------------------------------------------------------------- /app/jobs/send_to_datastore_job.rb: -------------------------------------------------------------------------------- 1 | class SendToDatastoreJob < ApplicationJob 2 | queue_as :default 3 | 4 | def perform(data_param, device_id) 5 | @device = Device.includes(:components).find(device_id) 6 | readings = JSON.parse(data_param) 7 | storer.store(@device, readings) 8 | end 9 | 10 | def storer 11 | @storer ||= Storer.new 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/lib/breadcrumb.rb: -------------------------------------------------------------------------------- 1 | class Breadcrumb < Struct.new(:breadcrumbs, :label, :url) 2 | def active? 3 | self == breadcrumbs.last 4 | end 5 | end 6 | 7 | -------------------------------------------------------------------------------- /app/lib/data_csv_parser.rb: -------------------------------------------------------------------------------- 1 | require "csv" 2 | class DataCSVParser 3 | 4 | def parse(string) 5 | csv = CSV.parse(string) 6 | sensor_ids = csv[3][1..].map(&:to_i) 7 | reading_rows = csv[4..-1] 8 | reading_rows.map { |row| 9 | recorded_at = row.shift 10 | sensors = row.zip(sensor_ids).flat_map { |value, sensor_id| 11 | if value != "null" 12 | { 13 | id: sensor_id.to_i, 14 | value: value.to_f 15 | } 16 | end 17 | }.compact 18 | if sensors.any? 19 | { recorded_at: recorded_at, sensors: sensors } 20 | end 21 | }.compact 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/lib/delete_response_headers.rb: -------------------------------------------------------------------------------- 1 | class DeleteResponseHeaders 2 | 3 | def initialize(app) 4 | @app = app 5 | end 6 | 7 | def call(env) 8 | status, headers, response = @app.call(env) 9 | # headers = headers.slice('Link', 'Per-Page', 'Total') unless env['QUERY_STRING'].include? "allheaders" 10 | headers = {} if env['QUERY_STRING'] == "noheaders" 11 | [status, headers, response] 12 | end 13 | 14 | end 15 | -------------------------------------------------------------------------------- /app/lib/header_check.rb: -------------------------------------------------------------------------------- 1 | class HeaderCheck 2 | 3 | def initialize(app) 4 | @app = app 5 | end 6 | 7 | def call(env) 8 | if env['REQUEST_PATH'].try('match',/\A\/v\d/) and env['HTTP_ACCEPT'].try('include?', 'application/vnd.smartcitizen') 9 | env.delete('HTTP_ACCEPT') 10 | end 11 | status, headers, response = @app.call(env) 12 | [status, headers, response] 13 | end 14 | 15 | end 16 | -------------------------------------------------------------------------------- /app/lib/mqtt_client_factory.rb: -------------------------------------------------------------------------------- 1 | module MQTTClientFactory 2 | def self.create_client(args={}, &block) 3 | host = args.fetch(:host, default_host) 4 | port = args.fetch(:port, default_port) 5 | clean_sesion = args.fetch(:clean_session, default_clean_session) 6 | client_id = args.fetch(:client_id, default_client_id) 7 | ssl = args.fetch(:ssl, default_ssl) 8 | MQTT::Client.connect( 9 | { host: host, port: port, clean_session: clean_sesion, client_id: client_id, ssl: ssl}, 10 | &block 11 | ) 12 | end 13 | 14 | def self.default_host 15 | ENV.fetch('MQTT_HOST', 'mqtt') 16 | end 17 | 18 | def self.default_port 19 | ENV.fetch('MQTT_PORT', "1883").to_i 20 | end 21 | 22 | def self.default_clean_session 23 | ENV.fetch('MQTT_CLEAN_SESSION', "true") == "true" 24 | end 25 | 26 | def self.default_client_id 27 | ENV.fetch('MQTT_CLIENT_ID', nil) 28 | end 29 | 30 | def self.default_ssl 31 | ENV.fetch('MQTT_SSL', "false") == "true" 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/lib/mqtt_forwarder.rb: -------------------------------------------------------------------------------- 1 | class MQTTForwarder 2 | def initialize(client) 3 | @client = client 4 | @prefix = prefix 5 | @suffix = suffix 6 | end 7 | 8 | def forward_readings(token, device_id, reading) 9 | topic = topic_path(token, device_id) 10 | client.publish(topic, reading) 11 | end 12 | 13 | private 14 | 15 | def topic_path(token, device_id) 16 | ["/forward", token, "device", device_id, "readings"].join("/") 17 | end 18 | 19 | attr_reader :client, :prefix, :suffix 20 | end 21 | -------------------------------------------------------------------------------- /app/lib/presenters.rb: -------------------------------------------------------------------------------- 1 | module Presenters 2 | # This is work in progress we're releasing early so 3 | # that it can be used in forwarding to send the current 4 | # values as they're received. 5 | # TODO: add presenter tests 6 | # use in appropriate views, delete unneeded code in models and views. 7 | PRESENTERS = { 8 | Device => Presenters::DevicePresenter, 9 | User => Presenters::UserPresenter, 10 | Component => Presenters::ComponentPresenter, 11 | Sensor => Presenters::SensorPresenter, 12 | Measurement => Presenters::MeasurementPresenter, 13 | } 14 | 15 | def self.present(model_or_collection, user, render_context, options={}) 16 | if model_or_collection.is_a?(Enumerable) 17 | model_or_collection.map { |model| present(model, user, render_context, options) } 18 | else 19 | PRESENTERS[model_or_collection.class]&.new( 20 | model_or_collection, user, render_context, options 21 | ).as_json 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/lib/presenters/measurement_presenter.rb: -------------------------------------------------------------------------------- 1 | module Presenters 2 | class MeasurementPresenter < BasePresenter 3 | 4 | alias_method :measurement, :model 5 | 6 | def exposed_fields 7 | %i{id name description unit uuid definition} 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/lib/presenters/sensor_presenter.rb: -------------------------------------------------------------------------------- 1 | module Presenters 2 | class SensorPresenter < BasePresenter 3 | 4 | alias_method :sensor, :model 5 | 6 | def exposed_fields 7 | %i{id parent_id name description unit created_at updated_at uuid datasheet unit_definition measurement tags} 8 | end 9 | 10 | def measurement 11 | present(sensor.measurement) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/lib/presenters/user_presenter.rb: -------------------------------------------------------------------------------- 1 | module Presenters 2 | class UserPresenter < BasePresenter 3 | 4 | alias_method :user, :model 5 | 6 | def default_options 7 | { 8 | with_devices: true 9 | } 10 | end 11 | 12 | def exposed_fields 13 | %i{id uuid role username profile_picture url location email legacy_api_key devices created_at updated_at} 14 | end 15 | 16 | def profile_picture 17 | render_context&.profile_picture_url(user) 18 | end 19 | 20 | def email 21 | authorize!(:email) { user.email } 22 | end 23 | 24 | def legacy_api_key 25 | authorize!(:legacy_api_key) { user.legacy_api_key } 26 | end 27 | 28 | def devices 29 | present(user.devices) if options[:with_devices] 30 | end 31 | 32 | private 33 | 34 | def authorized? 35 | policy.show_private_info? 36 | end 37 | 38 | def policy 39 | @policy ||= UserPolicy.new(current_user, user) 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /app/lib/pretty_json.rb: -------------------------------------------------------------------------------- 1 | # require 'action_controller/metal/renderers' 2 | 3 | module PrettyJSON 4 | 5 | # ActionController::Renderers.remove :json 6 | # # http://stackoverflow.com/a/23018176 7 | # ActionController::Renderers.add :json do |json, options| 8 | # raise 'fuck' 9 | # unless json.kind_of?(String) 10 | # if params[:pretty] 11 | # json = json.as_json(options) if json.respond_to?(:as_json) 12 | # json = JSON.pretty_generate(json, options) 13 | # else 14 | # json = json.to_json(options) 15 | # end 16 | # end 17 | 18 | # # if params[:callback].present? 19 | # # self.content_type ||= Mime::JS 20 | # # "#{params[:callback]}(#{json})" 21 | # # else 22 | # # self.content_type ||= Mime::JSON 23 | # # json 24 | # # end 25 | # end 26 | 27 | end 28 | -------------------------------------------------------------------------------- /app/lib/raw_mqtt_message_parser.rb: -------------------------------------------------------------------------------- 1 | require_relative "../grammars/raw_message" 2 | 3 | class RawMqttMessageParser 4 | def initialize 5 | @parser = RawMessageParser.new 6 | end 7 | 8 | def parse(message) 9 | message = parser.parse(self.convert_to_ascii(message.strip))&.to_hash 10 | raise "Message not parsed: #{message}" unless message 11 | return message 12 | end 13 | 14 | private 15 | 16 | def convert_to_ascii(string) 17 | string.encode("US-ASCII", invalid: :replace, undef: :replace, replace: "") 18 | end 19 | 20 | attr_reader :parser 21 | end 22 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: "SmartCitizen Notifications ", 3 | reply_to: "SmartCitizen Support " 4 | end 5 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /app/models/concerns/archive_workflow.rb: -------------------------------------------------------------------------------- 1 | # issue with regular concerns https://github.com/geekq/workflow/issues/152 2 | 3 | module ArchiveWorkflow 4 | 5 | def self.included(base) 6 | 7 | base.scope :with_active_state, -> { where(workflow_state: :active) } 8 | base.scope :with_archived_state, -> { where(workflow_state: :archived) } 9 | 10 | base.workflow do 11 | state :active do 12 | event :archive, :transitions_to => :archived 13 | end 14 | state :archived do 15 | event :unarchive, :transitions_to => :active 16 | end 17 | after_transition do 18 | 19 | begin 20 | if archived? 21 | pg_search_document.destroy 22 | elsif active? 23 | update_pg_search_document 24 | end 25 | rescue 26 | end 27 | 28 | if respond_to?(:owner_id) and owner_id.present? 29 | User.unscoped.find(owner_id).update_all_device_ids! 30 | end 31 | 32 | end 33 | end 34 | end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /app/models/concerns/country_methods.rb: -------------------------------------------------------------------------------- 1 | module CountryMethods 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | 6 | def country_code 7 | super.try(:upcase) 8 | end 9 | 10 | end 11 | 12 | def country 13 | if country_code and country_code.match /\A\w{2}\z/ 14 | ISO3166::Country[country_code] 15 | end 16 | end 17 | 18 | def country_name 19 | country ? country.to_s : nil 20 | end 21 | 22 | end 23 | -------------------------------------------------------------------------------- /app/models/concerns/message_forwarding.rb: -------------------------------------------------------------------------------- 1 | module MessageForwarding 2 | 3 | extend ActiveSupport::Concern 4 | 5 | def forward_readings(device, readings) 6 | if device.forward_readings? 7 | MQTTForwardingJob.perform_later(device.id, readings: readings.map(&:stringify_keys)) 8 | end 9 | end 10 | 11 | end 12 | -------------------------------------------------------------------------------- /app/models/device_inventory.rb: -------------------------------------------------------------------------------- 1 | class DeviceInventory < ActiveRecord::Base 2 | 3 | self.table_name = 'devices_inventory' 4 | 5 | def report 6 | self[:report] 7 | end 8 | 9 | def report=(str) 10 | self[:report] = JSON.parse(str) rescue nil 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /app/models/devices_tag.rb: -------------------------------------------------------------------------------- 1 | # A Device can have many Tags, and a Tag can have many Devices. 2 | 3 | class DevicesTag < ActiveRecord::Base 4 | belongs_to :device 5 | belongs_to :tag 6 | end 7 | -------------------------------------------------------------------------------- /app/models/ingest_error.rb: -------------------------------------------------------------------------------- 1 | class IngestError < ActiveRecord::Base 2 | belongs_to :device 3 | end 4 | -------------------------------------------------------------------------------- /app/models/measurement.rb: -------------------------------------------------------------------------------- 1 | # Measurements are descriptions of what sensors do. 2 | class Measurement < ActiveRecord::Base 3 | has_many :sensors 4 | validates_presence_of :name, :description 5 | validates_uniqueness_of :name 6 | 7 | def for_sensor_json 8 | attributes.except(*%w{created_at updated_at}) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/models/orphan_device.rb: -------------------------------------------------------------------------------- 1 | class OrphanDevice < ActiveRecord::Base 2 | attr_readonly :onboarding_session, :device_token 3 | 4 | validates_uniqueness_of :device_token 5 | validates_uniqueness_of :onboarding_session 6 | 7 | validates_presence_of :device_token, allow_nil: false 8 | validates_presence_of :onboarding_session, allow_nil: false 9 | 10 | validates :exposure, inclusion: { in: %w(indoor outdoor) }, allow_nil: true 11 | 12 | after_initialize :generate_onbarding_session 13 | 14 | def device_attributes 15 | { 16 | name: name, 17 | description: description, 18 | user_tags: user_tags, 19 | exposure: exposure, 20 | latitude: latitude, 21 | longitude: longitude, 22 | device_token: device_token 23 | } 24 | end 25 | 26 | def generate_token! 27 | self.device_token = SecureRandom.alphanumeric(6).downcase 28 | end 29 | 30 | private 31 | 32 | def generate_onbarding_session 33 | self.onboarding_session = SecureRandom.uuid if new_record? 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/models/postprocessing.rb: -------------------------------------------------------------------------------- 1 | class Postprocessing < ApplicationRecord 2 | belongs_to :device 3 | 4 | def self.ransackable_attributes(auth_object = nil) 5 | ["blueprint_url", "created_at", "device_id", "forwarding_params", "hardware_url", "id", "latest_postprocessing", "meta", "updated_at"] 6 | end 7 | 8 | def self_ransackable_associations(auth_object = nil) 9 | [] 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/models/sensor.rb: -------------------------------------------------------------------------------- 1 | # Every Device has one or more sensors. 2 | 3 | class Sensor < ActiveRecord::Base 4 | 5 | has_many :components 6 | has_many :devices, through: :components 7 | 8 | has_many :sensor_tags 9 | has_many :tag_sensors, through: :sensor_tags 10 | 11 | belongs_to :measurement, optional: :true 12 | 13 | attr_accessor :latest_reading 14 | 15 | has_ancestry 16 | validates_presence_of :name, :description#, :unit 17 | 18 | def self.ransackable_attributes(auth_object = nil) 19 | ["ancestry", "created_at", "description", "id", "measurement_id", "name", "unit", "updated_at", "uuid"] 20 | end 21 | 22 | def self.ransackable_associations(auth_object = nil) 23 | [] 24 | end 25 | 26 | def tags 27 | tag_sensors.map(&:name) 28 | end 29 | 30 | def is_raw? 31 | tags&.include?("raw") 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/models/sensor_tag.rb: -------------------------------------------------------------------------------- 1 | class SensorTag < ActiveRecord::Base 2 | belongs_to :sensor 3 | belongs_to :tag_sensor 4 | end 5 | -------------------------------------------------------------------------------- /app/models/tag.rb: -------------------------------------------------------------------------------- 1 | # Tags can be assigned to multiple Devices. They are currently managed solely by 2 | # admins, this is likely to change. 3 | 4 | class Tag < ActiveRecord::Base 5 | validates_uniqueness_of :name, case_sensitive: false 6 | validates_presence_of :name 7 | # validates_format_of :name, with: /\A[A-Za-z]+\z/ 8 | has_many :devices_tags, dependent: :destroy 9 | has_many :devices, through: :devices_tags 10 | 11 | extend FriendlyId 12 | friendly_id :name 13 | 14 | def self.ransackable_attributes(auth_object = nil) 15 | ["created_at", "description", "id", "name", "updated_at", "uuid"] 16 | end 17 | 18 | def ransackable_associations(auth_object = nil) 19 | [] 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/models/tag_sensor.rb: -------------------------------------------------------------------------------- 1 | class TagSensor < ActiveRecord::Base 2 | validates_presence_of :name 3 | 4 | has_many :sensor_tags 5 | has_many :sensors, through: :sensor_tags 6 | end 7 | -------------------------------------------------------------------------------- /app/models/validators/datetime_validator.rb: -------------------------------------------------------------------------------- 1 | class DatetimeValidator < ActiveModel::EachValidator 2 | def validate_each(record, attribute, value) 3 | [:starts_at, :ends_at].each do |field| 4 | if value.is_a?(String) 5 | begin 6 | Date.iso8601(value) 7 | rescue Date::Error 8 | begin 9 | Time.iso8601(value) 10 | rescue ArgumentError 11 | record.errors.add(attribute, "is not a valid ISO8601 date or time") 12 | end 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/policies/admin_policy.rb: -------------------------------------------------------------------------------- 1 | class AdminPolicy < ApplicationPolicy 2 | 3 | def show? 4 | true 5 | end 6 | 7 | def create? 8 | user.try(:is_admin?) 9 | end 10 | 11 | def destroy? 12 | create? 13 | end 14 | 15 | def update? 16 | create? 17 | end 18 | 19 | end 20 | -------------------------------------------------------------------------------- /app/policies/application_policy.rb: -------------------------------------------------------------------------------- 1 | class ApplicationPolicy 2 | attr_reader :user, :record 3 | 4 | def initialize(user, record) 5 | @user = user 6 | @record = record 7 | end 8 | 9 | def index? 10 | false 11 | end 12 | 13 | def show? 14 | scope.where(:id => record.id).exists? 15 | end 16 | 17 | def create? 18 | false 19 | end 20 | 21 | def new? 22 | create? 23 | end 24 | 25 | def update? 26 | false 27 | end 28 | 29 | def edit? 30 | update? 31 | end 32 | 33 | def destroy? 34 | false 35 | end 36 | 37 | def scope 38 | Pundit.policy_scope!(user, record.class) 39 | end 40 | 41 | class Scope 42 | attr_reader :user, :scope 43 | 44 | def initialize(user, scope) 45 | @user = user 46 | @scope = scope 47 | end 48 | 49 | def resolve 50 | scope 51 | end 52 | end 53 | end 54 | 55 | -------------------------------------------------------------------------------- /app/policies/component_policy.rb: -------------------------------------------------------------------------------- 1 | class ComponentPolicy < AdminPolicy 2 | end 3 | -------------------------------------------------------------------------------- /app/policies/experiment_policy.rb: -------------------------------------------------------------------------------- 1 | class ExperimentPolicy < ApplicationPolicy 2 | def update? 3 | user.try(:is_admin?) || user == record.owner 4 | end 5 | 6 | def create? 7 | user 8 | end 9 | 10 | def destroy? 11 | update? 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/policies/measurement_policy.rb: -------------------------------------------------------------------------------- 1 | class MeasurementPolicy < AdminPolicy 2 | end 3 | -------------------------------------------------------------------------------- /app/policies/oauth_application_policy.rb: -------------------------------------------------------------------------------- 1 | class OauthApplicationPolicy < ApplicationPolicy 2 | 3 | def show? 4 | true 5 | end 6 | 7 | def update? 8 | user.try(:is_admin?) || user == record.owner 9 | end 10 | 11 | def create? 12 | user 13 | end 14 | 15 | def destroy? 16 | update? 17 | end 18 | 19 | end 20 | -------------------------------------------------------------------------------- /app/policies/password_reset_policy.rb: -------------------------------------------------------------------------------- 1 | class PasswordResetPolicy < ApplicationPolicy 2 | 3 | def show? 4 | true 5 | end 6 | 7 | def create? 8 | user 9 | end 10 | 11 | def update? 12 | user == record 13 | end 14 | 15 | end 16 | -------------------------------------------------------------------------------- /app/policies/sensor_policy.rb: -------------------------------------------------------------------------------- 1 | class SensorPolicy < AdminPolicy 2 | end 3 | -------------------------------------------------------------------------------- /app/policies/session_policy.rb: -------------------------------------------------------------------------------- 1 | class SessionPolicy < ApplicationPolicy 2 | 3 | def create? 4 | true 5 | end 6 | 7 | end 8 | -------------------------------------------------------------------------------- /app/policies/tag_policy.rb: -------------------------------------------------------------------------------- 1 | class TagPolicy < AdminPolicy 2 | end 3 | -------------------------------------------------------------------------------- /app/policies/tag_sensor_policy.rb: -------------------------------------------------------------------------------- 1 | class TagSensorPolicy < AdminPolicy 2 | end 3 | -------------------------------------------------------------------------------- /app/policies/user_policy.rb: -------------------------------------------------------------------------------- 1 | class UserPolicy < ApplicationPolicy 2 | 3 | def show? 4 | true 5 | end 6 | 7 | def create? 8 | !user || user.is_admin? 9 | end 10 | 11 | def update? 12 | user.try(:is_admin?) || user == record 13 | end 14 | 15 | def destroy? 16 | user == record 17 | end 18 | 19 | def request_password_reset? 20 | create? 21 | end 22 | 23 | def update_password? 24 | create? 25 | end 26 | 27 | def show_private_info? 28 | update? 29 | end 30 | 31 | def show_secrets? 32 | user.try(:is_admin?) || user == record 33 | end 34 | 35 | def register_device? 36 | user == record 37 | end 38 | 39 | def create_experiment? 40 | user == record 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /app/views/layouts/_breadcrumbs.html.erb: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /app/views/layouts/_doorbell_embed.html.erb: -------------------------------------------------------------------------------- 1 | 14 | 15 | -------------------------------------------------------------------------------- /app/views/layouts/_flashes.html.erb: -------------------------------------------------------------------------------- 1 | <% flash.each do |name, msg| %> 2 |
"> 3 |
4 | <%= msg %> 5 | 6 |
7 |
8 | <% end %> 9 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= [t(:title), @title].compact.join(" – ") %> 5 | 6 | 7 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> 8 | <%= javascript_pack_tag 'application', 'data-turbolinks-track' => true %> 9 | <%= csrf_meta_tags %> 10 | 11 | 12 | 13 | <%= render partial: "layouts/nav" %> 14 | <%= render partial: "layouts/flashes" %> 15 |
16 | <%= content_for?(:container) ? yield(:container) : yield %> 17 |
18 | <%= render partial: "layouts/footer" %> 19 | <%= render partial: "layouts/doorbell_embed" %> 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/views/ui/devices/_actions.html.erb: -------------------------------------------------------------------------------- 1 |

2 | <%= sc_nav_button_to(t(:show_device_on_map_cta), "https://smartcitizen.me/kits/#{device.id}", local_assigns.merge(external: true)) %> 3 | <% if authorize? device, :update? %> 4 | <%= sc_nav_button_to(t(:show_device_edit_cta, name: device.name), edit_ui_device_path(device, goto: request.path), local_assigns) %> 5 | <% end %> 6 | <% if authorize? device, :upload? %> 7 | <%= sc_nav_button_to(t(:show_device_upload_cta), upload_ui_device_path(device, goto: request.path), local_assigns) %> 8 | <% end %> 9 | <% if authorize? device, :download? %> 10 | <%= sc_nav_button_to(t(:show_device_download_cta), download_ui_device_path(device, goto: request.path), local_assigns) %> 11 | <% end %> 12 |

13 | -------------------------------------------------------------------------------- /app/views/ui/devices/_device.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= link_to(ui_device_path(device.id), class: "listing-link z-1") do %> 3 | <%= device.name %> 4 | <% end %> 5 |
6 | <% if device.latitude && device.longitude %> 7 | <%= render partial: "ui/shared/map", locals: { points: [{lat: device.latitude, lng: device.longitude}] } %> 8 | <% end %> 9 |
10 |
11 |

<%= device.name %>

12 | <%= render partial: "ui/devices/meta", locals: { device: device, hide_owner: local_assigns[:hide_owner] } %> 13 | <% if local_assigns[:with_actions] %> 14 |
15 | <%= render partial: "ui/devices/actions", locals: { device: device } %> 16 |
17 | <% end %> 18 |
19 |
20 | -------------------------------------------------------------------------------- /app/views/ui/devices/_profile_header.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :profile_header_image do %> 2 |
3 | <%= render partial: "ui/shared/map", locals: { points: [{lat: device.latitude, lng: device.longitude}] } %> 4 |
5 | <% end %> 6 | <% content_for :profile_header_meta do %> 7 | <%= render partial: "ui/devices/meta", locals: { device: device, hide_last_reading_at: true } %> 8 | <% end %> 9 | <% content_for :profile_header_actions do %> 10 | <%= render partial: "ui/devices/actions", locals: { device: device, dark_buttons: true } %> 11 | <% end %> 12 | <%= render partial: "ui/shared/profile_header", locals: { title: t(:show_device_headline, name: device.name), title_link: ui_device_path(device.id) } %> 13 | -------------------------------------------------------------------------------- /app/views/ui/devices/delete.html.erb: -------------------------------------------------------------------------------- 1 | <%= render partial: "ui/devices/profile_header", locals: { device: @device } %> 2 | <%= render layout: "ui/shared/form_container", locals: { title: t(:delete_device_header) } do %> 3 | <%= bootstrap_form_tag url: ui_device_path(@device.id), method: :delete do |f| %> 4 |

<%= t(:delete_device_warning_html, name: @device.name) %>

5 | <%= f.text_field :name, label: t(:delete_device_name_label) %> 6 |
7 | <%= f.primary t(:delete_device_submit), class: "btn btn-danger w-100 w-md-auto" %> 8 |
9 | <% end %> 10 | <% end %> 11 | -------------------------------------------------------------------------------- /app/views/ui/devices/download.html.erb: -------------------------------------------------------------------------------- 1 | <%= render partial: "ui/devices/profile_header", locals: { device: @device } %> 2 | <%= render layout: "ui/shared/form_container", locals: { title: t(:download_device_header) } do %> 3 | <% if @device.csv_export_requested_recently? %> 4 |

<%= t(:download_device_recently_requested_blurb) %>

5 |
6 | <%= link_to(t(:download_device_back), ui_device_path(@device.id), class: "btn btn-secondary w-100 w-md-auto") %> 7 |
8 | <% else %> 9 | <%= bootstrap_form_tag url: download_ui_device_path(@device.id), method: :post do |f| %> 10 | <%= hidden_field_tag :goto, params[:goto] %> 11 |

<%= t(:download_device_confirmation_blurb) %>

12 |
13 | <%= f.primary t(:download_device_submit), class: "btn btn-primary w-100 w-md-auto" %> 14 |
15 | <% end %> 16 | <% end %> 17 | <% end %> 18 | 19 | -------------------------------------------------------------------------------- /app/views/ui/devices/edit.html.erb: -------------------------------------------------------------------------------- 1 | <%= render partial: "ui/devices/profile_header", locals: { device: @device } %> 2 | <%= render layout: "ui/shared/form_container", locals: { title: t(:edit_device_header) } do %> 3 | <%= bootstrap_form_for @device, url: ui_device_path(@device) do |form| %> 4 | <%= hidden_field_tag :goto, params[:goto] %> 5 | <%= render partial: "fields", locals: { device: @device, form: form } %> 6 | <%= render partial: "ui/shared/form_buttons", locals: { form: form, back_href: ui_device_path(@device.id), submit_label: t(:edit_device_submit) } %> 7 | <% end %> 8 | <% if authorize? @device, :destroy? %> 9 | <%= render layout: "ui/shared/danger_zone" do %> 10 |
<%= link_to t(:edit_device_delete_device_submit), delete_ui_device_path(@device.id), class: "btn btn-danger w-100 w-md-25 justify-content-center" %>
11 | <% end %> 12 | <% end %> 13 | <% end %> 14 | -------------------------------------------------------------------------------- /app/views/ui/devices/new.html.erb: -------------------------------------------------------------------------------- 1 | <%= render partial: "ui/users/header", locals: { user: current_user } %> 2 | <%= render layout: "ui/shared/form_container" do %> 3 | <%= bootstrap_form_for @device, url: ui_devices_path do |form| %> 4 | <%= render partial: "fields", locals: { device: @device, form: form, include_hardware_info: true } %> 5 | <%= render partial: "ui/shared/form_buttons", locals: { form: form, back_href: register_ui_devices_path, submit_label: t(:new_device_submit) } %> 6 | <% end %> 7 | <% end %> 8 | -------------------------------------------------------------------------------- /app/views/ui/devices/upload.html.erb: -------------------------------------------------------------------------------- 1 | <%= render partial: "ui/devices/profile_header", locals: { device: @device } %> 2 | <%= render layout: "ui/shared/form_container", locals: { title: t(:upload_device_header) } do %> 3 | <%= bootstrap_form_tag url: upload_ui_device_path(@device.id), method: :post, multipart: true do |f| %> 4 | <%= hidden_field_tag :goto, params[:goto] %> 5 |

<%= t(:upload_device_blurb) %>

6 | <%= f.file_field :data_files, multiple: true, label: t(:upload_device_data_files_label), help: t(:upload_device_data_files_help), name: "data_files[]" %> 7 |
8 | <%= f.primary t(:upload_device_submit), class: "btn btn-primary w-100 w-md-auto" %> 9 |
10 | <% end %> 11 | <% end %> 12 | 13 | -------------------------------------------------------------------------------- /app/views/ui/experiments/_actions.html.erb: -------------------------------------------------------------------------------- 1 | <% button_class = local_assigns[:dark_buttons] ? "btn-dark" : "btn-secondary" %> 2 |

3 | <% if authorize? experiment, :update? %> 4 | <%= sc_nav_button_to(t(:show_experiment_edit_cta, name: experiment.name), edit_ui_experiment_path(experiment, goto: request.path), local_assigns) %> 5 | <% end %> 6 |

7 | -------------------------------------------------------------------------------- /app/views/ui/experiments/_devices_collection.html.erb: -------------------------------------------------------------------------------- 1 | <%= render layout: "ui/shared/box", locals: { no_padding: true } do %> 2 |
3 |

<%= t :experiment_devices_collection_heading %>

4 | <% if devices.length < 1 %> 5 |

<%= t :experiment_devices_collection_no_devices_message %>

6 | <% end %> 7 |
8 |
"> 9 | <%= render partial: "ui/devices/device", collection: devices.by_last_reading, locals: { with_actions: true } %> 10 |
11 | <% if devices.total_pages > 1 %> 12 |
13 |
14 | <%= paginate devices, param_name: :device_page, theme: "bootstrap-5" %> 15 |
16 |
17 | <% end %> 18 | <% end %> 19 | -------------------------------------------------------------------------------- /app/views/ui/experiments/_header.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :profile_header_image do %> 2 |
3 | <%= render partial: "ui/shared/map", 4 | locals: { 5 | points: experiment.devices.map { |d| { lat: d.latitude, lng: d.longitude } }, 6 | class: "experiment", 7 | marker_url: asset_url("map-pin-experiment.svg") 8 | } 9 | %> 10 |
11 | <% end %> 12 | <% content_for :profile_header_meta do %> 13 | <%= render partial: "ui/experiments/meta", locals: { experiment: experiment } %> 14 | <% end %> 15 | <% content_for :profile_header_actions do %> 16 | <%= render partial: "ui/experiments/actions", locals: { experiment: experiment, dark_buttons: true} %> 17 | <% end %> 18 | <%= render partial: "ui/shared/profile_header", locals: { title: t(:show_experiment_title, name: experiment.name), title_link: ui_experiment_path(experiment.id) } %> 19 | -------------------------------------------------------------------------------- /app/views/ui/experiments/_meta.html.erb: -------------------------------------------------------------------------------- 1 |

"> 2 | <%= show_svg("status_icon.svg") %> 3 | <%= pluralize(experiment.devices.length, t(:experiment_meta_device_noun)) %> (<%= experiment.online_device_count %> <%= t(:experiment_meta_online_adjective) %>) 4 |

5 | <% unless local_assigns[:hide_owner] %> 6 |

7 | <%= show_svg("user_details_icon_light.svg") %> 8 | <%= link_to(experiment.owner.username, ui_user_path(experiment.owner.username), class: "d-inline-block position-relative subtle-link z-3") %> 9 |

10 | <% end %> 11 |

12 | <%= show_svg("clock-fill.svg") %> 13 | <% if experiment.last_reading_at %> 14 | <%= t :device_meta_last_reading_at, time: time_ago_in_words(experiment.last_reading_at) %> 15 | <% else %> 16 | <%= t :device_meta_no_readings_message %> 17 | <% end %> 18 |

19 | -------------------------------------------------------------------------------- /app/views/ui/experiments/delete.html.erb: -------------------------------------------------------------------------------- 1 | <%= render partial: "ui/experiments/header", locals: { experiment: @experiment } %> 2 | <%= render layout: "ui/shared/form_container", locals: { title: t(:delete_experiment_header), box_inner_class: "drop-shadow-teal" } do %> 3 | <%= bootstrap_form_tag url: ui_experiment_path(@experiment.id), method: :delete do |f| %> 4 |

<%= t(:delete_experiment_warning_html, name: @experiment.name) %>

5 | <%= f.text_field :name, label: t(:delete_experiment_name_label) %> 6 |
7 | <%= f.primary t(:delete_experiment_submit), class: "btn btn-danger w-100 w-md-auto" %> 8 |
9 | <% end %> 10 | <% end %> 11 | -------------------------------------------------------------------------------- /app/views/ui/experiments/new.html.erb: -------------------------------------------------------------------------------- 1 | <%= render partial: "ui/users/header", locals: { user: current_user } %> 2 | <%= render layout: "ui/shared/form_container" do %> 3 |

<%= t(:new_experiment_blurb) %>

4 | <%= bootstrap_form_for @experiment, url: ui_experiments_path do |form| %> 5 | <%= render partial: "fields", locals: { experiment: @experiment, form: form, include_hardware_info: true } %> 6 | <%= render partial: "ui/shared/form_buttons", locals: { form: form, back_href: ui_user_path(current_user.username), submit_label: t(:new_experiment_submit) } %> 7 | <% end %> 8 | <% end %> 9 | -------------------------------------------------------------------------------- /app/views/ui/sessions/password_reset_landing.html.erb: -------------------------------------------------------------------------------- 1 | <%= render layout: "ui/shared/form_container" do %> 2 | <%= bootstrap_form_tag url: ui_change_password_path do |f| %> 3 | <%= f.hidden_field :token, value: @token %> 4 | <%= f.password_field :password %> 5 | <%= f.password_field :password_confirmation, label: t(:users_password_reset_landing_confirmation_label) %> 6 | 7 | <%= render partial: "ui/shared/form_buttons", locals: {form: f, submit_label: t(:users_password_reset_landing_submit), submit_name: "change_password" } %> 8 | <% end %> 9 | <% end %> 10 | -------------------------------------------------------------------------------- /app/views/ui/shared/_box.html.erb: -------------------------------------------------------------------------------- 1 |
<%= local_assigns[:class] || "" %>"> 2 |
<%= "p-4 pb-md-5" unless local_assigns[:no_padding] %> <%= local_assigns[:inner_class] || "" %>"> 3 | <%= yield %> 4 |
5 |
6 | -------------------------------------------------------------------------------- /app/views/ui/shared/_container.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= yield %> 3 |
4 | -------------------------------------------------------------------------------- /app/views/ui/shared/_copyable_input.html.erb: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 8 |
9 |
10 | -------------------------------------------------------------------------------- /app/views/ui/shared/_danger_zone.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

<%= t(:danger_zone_subhead) %>

3 | <%= yield %> 4 |
5 | -------------------------------------------------------------------------------- /app/views/ui/shared/_extra_info.html.erb: -------------------------------------------------------------------------------- 1 | 2 | <%= image_tag("info_icon.svg") %> 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/views/ui/shared/_form_buttons.html.erb: -------------------------------------------------------------------------------- 1 |
gap-3"> 2 | <% if local_assigns[:back_href] %> 3 | 4 | <%= local_assigns[:back_label] || t(:back) %> 5 | 6 | <% end %> 7 | <%= form.primary submit_label, name: local_assigns[:submit_name], class: "btn btn-primary w-100 w-md-25 justify-content-center" %> 8 |
9 | -------------------------------------------------------------------------------- /app/views/ui/shared/_form_container.html.erb: -------------------------------------------------------------------------------- 1 | <%= render layout: "ui/shared/container" do %> 2 | <%= render layout: "ui/shared/box", locals: { inner_class: local_assigns[:box_inner_class] } do %> 3 | <%= render partial: "ui/shared/title", locals: { title: local_assigns[:title] } %> 4 | <%= yield %> 5 | <% end %> 6 | <% end %> 7 | -------------------------------------------------------------------------------- /app/views/ui/shared/_map.html.erb: -------------------------------------------------------------------------------- 1 |
" data-marker-shadow-url="<%= asset_url("map-pin-shadow.png") %>">
2 | -------------------------------------------------------------------------------- /app/views/ui/shared/_map_location_picker.html.erb: -------------------------------------------------------------------------------- 1 |
" data-marker-shadow-url="<%= asset_url("map-pin-shadow.png") %>">
2 | -------------------------------------------------------------------------------- /app/views/ui/shared/_title.html.erb: -------------------------------------------------------------------------------- 1 | <% if local_assigns[:title] || @title %> 2 |
3 |

<%= local_assigns[:title] || @title %>

4 |
5 | <% end %> 6 | -------------------------------------------------------------------------------- /app/views/ui/shared/_two_column.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | <%= yield :two_column_sidebar %> 5 |
6 |
7 | <%= yield :two_column_main %> 8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /app/views/ui/users/_actions.html.erb: -------------------------------------------------------------------------------- 1 |

2 | <% if authorize? user, :update? %> 3 | <%= sc_nav_button_to(t(:show_user_edit_cta, owner: possessive(user, current_user)), edit_ui_user_path(user), local_assigns) %> 4 | <% end %> 5 | <% if authorize? user, :show_secrets? %> 6 | <%= sc_nav_button_to(t(:show_user_secrets_cta, owner: possessive(user, current_user)), secrets_ui_user_path(user), local_assigns) %> 7 | <% end %> 8 | <% if authorize? user, :register_device? %> 9 | <%= sc_nav_button_to(t(:show_user_register_cta), register_ui_devices_path, local_assigns) %> 10 | <% end %> 11 | <% if authorize? user, :create_experiment? %> 12 | <%= sc_nav_button_to(t(:show_user_new_experiment_cta), new_ui_experiment_path, local_assigns) %> 13 | <% end %> 14 | <% if current_user == user %> 15 | <%= sc_nav_button_to(t(:show_user_log_out_cta), logout_path, local_assigns.merge(class: "hover-danger")) %> 16 | <% end %> 17 |

18 | -------------------------------------------------------------------------------- /app/views/ui/users/_header.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :profile_header_image do %> 2 | <%= render partial: "ui/users/profile_image", locals: { user: user } %> 3 | <% end %> 4 | <% content_for :profile_header_meta do %> 5 | <%= render partial: "ui/users/meta", locals: { user: user } %> 6 | <% end %> 7 | <% content_for :profile_header_actions do %> 8 | <%= render partial: "ui/users/actions", locals: { user: user, dark_buttons: true } %> 9 | <% end %> 10 | <%= render partial: "ui/shared/profile_header", locals: { title: t(:show_user_headline, username: user.username), title_link: ui_user_path(user.username) } %> 11 | 12 | -------------------------------------------------------------------------------- /app/views/ui/users/_meta.html.erb: -------------------------------------------------------------------------------- 1 | <% if user.city || user.country %> 2 |

3 | <%= show_svg("location_icon.svg") %> 4 | <%= [user.city, user.country].join(", ") %> 5 |

6 | <% end %> 7 | <% if user.url %> 8 |

9 | <%= image_tag("url_icon_light.svg", class: "pe-1") %> 10 | <%= link_to(user.url, user.url, class: "link-white") %> 11 |

12 | <% end %> 13 | -------------------------------------------------------------------------------- /app/views/ui/users/_no_device_cta.html.erb: -------------------------------------------------------------------------------- 1 | 2 |

<%= t :no_device_cta_heading %>

3 |

<%= t :no_device_cta_blurb %>

4 |
5 | <%= image_tag "smartcitizen-2-2-kit.gif", class: "w-100" %> 6 | <%= t :no_device_kit_link_text %> 7 | <%= t :no_device_onboarding_link_text %> 8 |
9 | -------------------------------------------------------------------------------- /app/views/ui/users/_no_experiment_cta.html.erb: -------------------------------------------------------------------------------- 1 |

<%= t :no_experiment_cta_heading %>

2 |

<%= t :no_experiment_cta_blurb %>

3 | 6 | -------------------------------------------------------------------------------- /app/views/ui/users/_page_nav.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= render layout: "ui/shared/box", locals: { no_padding: true, class: "w-100 sticky-top z-2 page-nav", no_shadow: true } do %> 3 | <%= t :show_user_devices_heading %> 4 | <%= t :show_user_experiments_heading %> 5 | <% end %> 6 |
7 | -------------------------------------------------------------------------------- /app/views/ui/users/_profile_image.html.erb: -------------------------------------------------------------------------------- 1 |
"> 2 | <%= image_tag( 3 | user.profile_picture.present? ? user.profile_picture : "default_avatar.svg", 4 | alt: local_assigns[:alt] || t(:profile_image_alt, username: user.username), 5 | class: "w-100" 6 | ) %> 7 |
8 | -------------------------------------------------------------------------------- /app/views/ui/users/delete.html.erb: -------------------------------------------------------------------------------- 1 | <%= render partial: "ui/users/header", locals: { user: @user } %> 2 | <%= render layout: "ui/shared/form_container" do %> 3 | <%= bootstrap_form_tag url: ui_user_path(@user.id), method: :delete do |f| %> 4 |

<%= t(:delete_user_warning_html, username: current_user.username) %>

5 | <%= f.text_field :username, label: t(:delete_user_username_label, owner: possessive(@user, current_user)) %> 6 |
7 | <%= f.primary t(:delete_user_submit, owner: possessive(@user, current_user, first_person: true)), class: "btn btn-danger w-100 w-md-auto" %> 8 |
9 | <% end %> 10 | <% end %> 11 | -------------------------------------------------------------------------------- /app/views/ui/users/new.html.erb: -------------------------------------------------------------------------------- 1 | <%= render layout: "ui/shared/form_container" do %> 2 | <%= bootstrap_form_for @user, url: ui_users_path do |f| %> 3 | <%= hidden_field_tag :goto, params[:goto] %> 4 | <%= f.text_field :email %> 5 | <%= f.text_field :username %> 6 | <%= f.password_field :password %> 7 | <%= f.password_field :password_confirmation %> 8 |
9 | <%= f.check_box :ts_and_cs, label: t(:new_user_ts_and_cs_label_html) %> 10 |
11 | <%= render partial: "ui/shared/form_buttons", locals: {form: f, back_href: new_ui_session_path, back_label: t(:new_user_login_link), submit_label: t(:new_user_submit) } %> 12 | <% end %> 13 | <% end %> 14 | -------------------------------------------------------------------------------- /app/views/ui/users/post_delete.html.erb: -------------------------------------------------------------------------------- 1 | <%= render layout: "ui/shared/form_container" do %> 2 |

<%= t(:post_delete_user_blurb_html)%>

3 | 6 | <% end %> 7 | -------------------------------------------------------------------------------- /app/views/ui/users/show.html.erb: -------------------------------------------------------------------------------- 1 | <%= render partial: "ui/users/header", locals: { user: @user } %> 2 | <% content_for :two_column_sidebar do %> 3 | <%= render partial: "ui/users/page_nav" %> 4 | <% end %> 5 | <% content_for :two_column_main do %> 6 | 7 | <%= render partial: "ui/users/devices_collection", locals: { user: @user, devices: @devices } %> 8 | <%= render partial: "ui/users/experiments_collection", locals: { user: @user, experiments: @experiments } %> 9 | <% end %> 10 | <%= render partial: "ui/shared/two_column" %> 11 | -------------------------------------------------------------------------------- /app/views/v0/components/_component.jbuilder: -------------------------------------------------------------------------------- 1 | json.(component, :id, :uuid, :device_id, :sensor_id, :created_at, :updated_at) 2 | -------------------------------------------------------------------------------- /app/views/v0/components/index.jbuilder: -------------------------------------------------------------------------------- 1 | json.array! @components, partial: 'component', as: :component 2 | -------------------------------------------------------------------------------- /app/views/v0/components/show.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! "component", component: @component 2 | -------------------------------------------------------------------------------- /app/views/v0/devices/_world_map_device.jbuilder: -------------------------------------------------------------------------------- 1 | json.( 2 | device, 3 | :id, 4 | :name, 5 | :description, 6 | :state, 7 | :system_tags, 8 | :user_tags, 9 | :last_reading_at, 10 | ) 11 | 12 | json.merge!(location: device.formatted_location(true)) 13 | json.merge!(hardware: device.hardware(false)) 14 | 15 | -------------------------------------------------------------------------------- /app/views/v0/devices/_world_map_list.jbuilder: -------------------------------------------------------------------------------- 1 | json.array! Device.for_world_map(never_authorized ? nil : current_user), partial: 'world_map_device', as: :device 2 | -------------------------------------------------------------------------------- /app/views/v0/devices/fresh_world_map.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! 'devices/world_map_list', { never_authorized: false } -------------------------------------------------------------------------------- /app/views/v0/devices/index.jbuilder: -------------------------------------------------------------------------------- 1 | json.array! @devices, partial: 'device', as: :device -------------------------------------------------------------------------------- /app/views/v0/devices/show.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! "device", device: @device 2 | -------------------------------------------------------------------------------- /app/views/v0/devices/world_map.jbuilder: -------------------------------------------------------------------------------- 1 | json.cache! ["world_map"], expires_in: 1.minute do 2 | json.partial! 'devices/world_map_list', { never_authorized: true} 3 | end -------------------------------------------------------------------------------- /app/views/v0/experiments/_experiment.jbuilder: -------------------------------------------------------------------------------- 1 | json.(experiment, 2 | :id, :name, :description, :owner_id, :active, :is_test, :starts_at, :ends_at, :device_ids, :created_at, :updated_at 3 | ) 4 | -------------------------------------------------------------------------------- /app/views/v0/experiments/index.jbuilder: -------------------------------------------------------------------------------- 1 | json.array! @experiments, partial: 'experiment', as: :experiment 2 | -------------------------------------------------------------------------------- /app/views/v0/experiments/show.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! "experiment", experiment: @experiment 2 | -------------------------------------------------------------------------------- /app/views/v0/measurements/_measurement.jbuilder: -------------------------------------------------------------------------------- 1 | json.(measurement, 2 | :id, :uuid, :name, :description, :definition, :created_at, :updated_at 3 | # :is_childless?, 4 | ) 5 | 6 | # json . sensor do 7 | # json.id sensor.id 8 | # json.parent_id sensor.parent_id 9 | # json.name sensor.name 10 | # json.description sensor.description 11 | # json.unit sensor.unit 12 | # json.created_at sensor.created_at.utc.iso8601 13 | # json.updated_at sensor.updated_at.utc.iso8601 14 | # end 15 | -------------------------------------------------------------------------------- /app/views/v0/measurements/index.jbuilder: -------------------------------------------------------------------------------- 1 | json.array! @measurements, partial: 'measurement', as: :measurement 2 | -------------------------------------------------------------------------------- /app/views/v0/measurements/show.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! "measurement", measurement: @measurement 2 | -------------------------------------------------------------------------------- /app/views/v0/oauth_applications/_oauth_application.jbuilder: -------------------------------------------------------------------------------- 1 | json.(oauth_application, :id, :name, :uid, :secret, :redirect_uri, :scopes, :created_at, :updated_at) 2 | -------------------------------------------------------------------------------- /app/views/v0/oauth_applications/index.jbuilder: -------------------------------------------------------------------------------- 1 | json.array! @oauth_applications, partial: 'oauth_application', as: :oauth_application 2 | -------------------------------------------------------------------------------- /app/views/v0/oauth_applications/show.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! "oauth_application", oauth_application: @oauth_application 2 | -------------------------------------------------------------------------------- /app/views/v0/sensors/_sensor.jbuilder: -------------------------------------------------------------------------------- 1 | json.(sensor, 2 | :id, :uuid, :parent_id, :name, :description, :unit, :tags, :datasheet, :unit_definition, :created_at, :updated_at 3 | # :is_childless?, 4 | ) 5 | 6 | if sensor.measurement 7 | json.measurement( 8 | sensor.measurement, :id, :uuid, :name, :description, :definition 9 | ) 10 | else 11 | json.merge! measurement: nil 12 | end 13 | 14 | # json . sensor do 15 | # json.id sensor.id 16 | # json.parent_id sensor.parent_id 17 | # json.name sensor.name 18 | # json.description sensor.description 19 | # json.unit sensor.unit 20 | # json.created_at sensor.created_at.utc.iso8601 21 | # json.updated_at sensor.updated_at.utc.iso8601 22 | # end 23 | -------------------------------------------------------------------------------- /app/views/v0/sensors/index.jbuilder: -------------------------------------------------------------------------------- 1 | json.array! @sensors, partial: 'sensor', as: :sensor 2 | -------------------------------------------------------------------------------- /app/views/v0/sensors/show.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! "sensor", sensor: @sensor 2 | -------------------------------------------------------------------------------- /app/views/v0/tag_sensors/index.jbuilder: -------------------------------------------------------------------------------- 1 | json.array! @tags 2 | -------------------------------------------------------------------------------- /app/views/v0/tag_sensors/show.jbuilder: -------------------------------------------------------------------------------- 1 | #json.(@tag, :id, :name, :description) 2 | json.merge! @tag.attributes 3 | -------------------------------------------------------------------------------- /app/views/v0/tags/_tag.jbuilder: -------------------------------------------------------------------------------- 1 | json.(tag, 2 | :id, :uuid, :name, :description, :created_at, :updated_at 3 | ) 4 | -------------------------------------------------------------------------------- /app/views/v0/tags/index.jbuilder: -------------------------------------------------------------------------------- 1 | json.array! @tags, partial: 'tag', as: :tag 2 | -------------------------------------------------------------------------------- /app/views/v0/tags/show.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! "tag", tag: @tag 2 | -------------------------------------------------------------------------------- /app/views/v0/users/_user.jbuilder: -------------------------------------------------------------------------------- 1 | json.(user, 2 | :id, 3 | :uuid, 4 | :role, 5 | :username, 6 | :profile_picture, 7 | :url, 8 | :location, 9 | :updated_at 10 | ) 11 | 12 | json.profile_picture profile_picture_url(user) 13 | 14 | authorized = current_user && current_user == user || current_user&.is_admin? 15 | 16 | if authorized 17 | json.merge! email: user.email 18 | json.merge! legacy_api_key: user.legacy_api_key 19 | json.merge! forwarding_token: user.forwarding_token 20 | json.merge! forwarding_username: user.forwarding_username 21 | else 22 | json.merge! email: '[FILTERED]' 23 | json.merge! legacy_api_key: '[FILTERED]' 24 | json.merge! forwarding_token: '[FILTERED]' 25 | json.merge! forwarding_username: '[FILTERED]' 26 | end 27 | 28 | json.devices user.devices.filter { |d| 29 | !d.is_private? || authorized 30 | }.map do |device| 31 | json.partial! "devices/device", device: device, with_data: false, with_owner: false, with_location: authorized 32 | end 33 | -------------------------------------------------------------------------------- /app/views/v0/users/index.jbuilder: -------------------------------------------------------------------------------- 1 | json.array! @users, partial: 'users/user', as: :user 2 | -------------------------------------------------------------------------------- /app/views/v0/users/show.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! "users/user", user: @user 2 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../config/application', __dir__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | require 'bundler/setup' 8 | load Gem.bin_path('rspec-core', 'rspec') 9 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads spring without using Bundler, in order to be fast. 4 | # It gets overwritten when you run the `spring binstub` command. 5 | 6 | unless defined?(Spring) 7 | require 'rubygems' 8 | require 'bundler' 9 | 10 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) 11 | spring = lockfile.specs.detect { |spec| spec.name == "spring" } 12 | if spring 13 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path 14 | gem 'spring', spring.version 15 | require 'spring/binstub' 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | include FileUtils 4 | 5 | # path to your application root. 6 | APP_ROOT = File.expand_path('..', __dir__) 7 | 8 | def system!(*args) 9 | system(*args) || abort("\n== Command #{args} failed ==") 10 | end 11 | 12 | chdir APP_ROOT do 13 | # This script is a way to update your development environment automatically. 14 | # Add necessary update steps to this file. 15 | 16 | puts '== Installing dependencies ==' 17 | system! 'gem install bundler --conservative' 18 | system('bundle check') || system!('bundle install') 19 | 20 | puts "\n== Updating database ==" 21 | system! 'bin/rails db:migrate' 22 | 23 | puts "\n== Removing old logs and tempfiles ==" 24 | system! 'bin/rails log:clear tmp:clear' 25 | 26 | puts "\n== Restarting application server ==" 27 | system! 'bin/rails restart' 28 | end 29 | -------------------------------------------------------------------------------- /bin/webpack: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" 4 | ENV["NODE_ENV"] ||= "development" 5 | 6 | require "pathname" 7 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 8 | Pathname.new(__FILE__).realpath) 9 | 10 | require "bundler/setup" 11 | 12 | require "webpacker" 13 | require "webpacker/webpack_runner" 14 | 15 | APP_ROOT = File.expand_path("..", __dir__) 16 | Dir.chdir(APP_ROOT) do 17 | Webpacker::WebpackRunner.run(ARGV) 18 | end 19 | -------------------------------------------------------------------------------- /bin/webpack-dev-server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" 4 | ENV["NODE_ENV"] ||= "development" 5 | 6 | require "pathname" 7 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 8 | Pathname.new(__FILE__).realpath) 9 | 10 | require "bundler/setup" 11 | 12 | require "webpacker" 13 | require "webpacker/dev_server_runner" 14 | 15 | APP_ROOT = File.expand_path("..", __dir__) 16 | Dir.chdir(APP_ROOT) do 17 | Webpacker::DevServerRunner.run(ARGV) 18 | end 19 | -------------------------------------------------------------------------------- /ci.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ae 3 | git config --global --add safe.directory /app 4 | source $NVM_DIR/nvm.sh 5 | nvm use default 6 | yarn install 7 | NODE_OPTIONS=--openssl-legacy-provider bundle exec bin/rake assets:precompile 8 | bundle exec bin/rake db:create 9 | bundle exec bin/rake db:schema:load 10 | unset DATABASE_URL 11 | RAILS_ENV=test bundle exec bin/rake db:create 12 | RAILS_ENV=test bundle exec bin/rake db:schema:load 13 | bundle exec bin/rake spec 14 | -------------------------------------------------------------------------------- /compose.override.local.yml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | restart: "no" 4 | sidekiq: 5 | restart: "no" 6 | mqtt-task-main-1: 7 | restart: "no" 8 | mqtt-task-main-2: 9 | restart: "no" 10 | mqtt-task-secondary: 11 | restart: "no" 12 | telnet-task: 13 | restart: "no" 14 | grafana: 15 | entrypoint: ["echo", "Grafana service disabled in development"] 16 | restart: "no" 17 | -------------------------------------------------------------------------------- /compose.override.production.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | services: 3 | app: 4 | build: 5 | args: 6 | - BUNDLE_WITHOUT=test development 7 | mqtt: 8 | entrypoint: ["echo", "MQTT service disabled in production"] 9 | cassandra-1: 10 | entrypoint: ["echo", "Cassandra service disabled in production"] 11 | kairos: 12 | depends_on: !reset [] 13 | -------------------------------------------------------------------------------- /compose.override.staging.yml: -------------------------------------------------------------------------------- 1 | services: 2 | db: 3 | deploy: 4 | resources: 5 | limits: 6 | memory: 2gb 7 | app: 8 | build: 9 | args: 10 | - BUNDLE_WITHOUT=test development 11 | mqtt: 12 | entrypoint: ["echo", "MQTT service disabled on staging"] 13 | mqtt-task-main-1: 14 | environment: 15 | MQTT_CLIENT_ID: smartcitizen-staging-api-main-1 16 | mqtt-task-main-2: 17 | environment: 18 | MQTT_CLIENT_ID: smartcitizen-staging-api-main-2 19 | mqtt-task-secondary: 20 | environment: 21 | MQTT_CLIENT_ID: "smartcitizen-staging-api-secondary" 22 | 23 | 24 | # cassandra-1: 25 | # entrypoint: ["echo", "Cassandra service disabled on staging"] 26 | # kairos: 27 | # depends_on: !reset [] 28 | -------------------------------------------------------------------------------- /compose.yml: -------------------------------------------------------------------------------- 1 | include: 2 | - compose/db.yml 3 | - compose/redis.yml 4 | - compose/app.yml 5 | - compose/sidekiq.yml 6 | - compose/mqtt-task.yml 7 | - compose/telnet-task.yml 8 | - compose/mqtt.yml 9 | - compose/web.yml 10 | - compose/kairos.yml 11 | - compose/cassandra.yml 12 | - compose/grafana.yml 13 | -------------------------------------------------------------------------------- /compose/db.yml: -------------------------------------------------------------------------------- 1 | services: 2 | db: 3 | image: postgres:10 4 | command: -c max_connections=200 5 | volumes: 6 | - sck-postgres:/var/lib/postgresql/data 7 | env_file: ../.env 8 | logging: 9 | driver: "json-file" 10 | options: 11 | max-size: "100m" 12 | #environment: 13 | # NOTE: Postgres 9.5 stopped allowing connections without passwords. 14 | # Enable this if needed. 15 | #- POSTGRES_HOST_AUTH_METHOD=trust 16 | volumes: 17 | sck-postgres: 18 | -------------------------------------------------------------------------------- /compose/grafana.yml: -------------------------------------------------------------------------------- 1 | services: 2 | grafana: 3 | image: grafana/agent 4 | env_file: ../.env 5 | volumes: 6 | - ../scripts/grafana/agent.yaml:/etc/agent/agent.yaml 7 | entrypoint: ["/bin/grafana-agent", "-config.expand-env", "--config.file=/etc/agent/agent.yaml","--metrics.wal-directory=/etc/agent/data"] -------------------------------------------------------------------------------- /compose/kairos.yml: -------------------------------------------------------------------------------- 1 | services: 2 | # There is no official docker image for Kairos 2017-12-18 3 | # kairos: 4 | # image: kairos 5 | kairos: 6 | env_file: ../.env 7 | build: 8 | context: ../scripts/ 9 | dockerfile: Dockerfile-kairos 10 | depends_on: 11 | cassandra-1: 12 | condition: service_healthy 13 | deploy: 14 | restart_policy: 15 | condition: on-failure 16 | max_attempts: 3 17 | window: 120s 18 | ports: 19 | - 8080:8080 20 | - 4242:4242 #telnet 21 | # We better not start Cassandra container in production, it eats up memory 22 | #depends_on: 23 | #- cassandra-1 -------------------------------------------------------------------------------- /compose/mqtt-task-common.yml: -------------------------------------------------------------------------------- 1 | services: 2 | mqtt-task: 3 | build: ../ 4 | env_file: ../.env 5 | command: ./mqtt_subscriber.sh 6 | restart: always 7 | volumes: 8 | - "../log:/app/log" 9 | logging: 10 | driver: "json-file" 11 | options: 12 | max-size: "100m" 13 | environment: 14 | db_pool_size: 5 15 | -------------------------------------------------------------------------------- /compose/mqtt-task.yml: -------------------------------------------------------------------------------- 1 | services: 2 | mqtt-task-main-1: 3 | extends: 4 | file: mqtt-task-common.yml 5 | service: mqtt-task 6 | environment: 7 | MQTT_CLIENT_ID: smartcitizen-api-main-1 8 | MQTT_CLEAN_SESSION: false 9 | mqtt-task-main-2: 10 | extends: 11 | file: mqtt-task-common.yml 12 | service: mqtt-task 13 | environment: 14 | MQTT_CLIENT_ID: smartcitizen-api-main-2 15 | MQTT_CLEAN_SESSION: false 16 | mqtt-task-secondary: 17 | extends: 18 | file: mqtt-task-common.yml 19 | service: mqtt-task 20 | environment: 21 | MQTT_CLIENT_ID: "smartcitizen-api-secondary" 22 | MQTT_CLEAN_SESSION: true 23 | deploy: 24 | mode: replicated 25 | replicas: 2 26 | 27 | 28 | -------------------------------------------------------------------------------- /compose/redis.yml: -------------------------------------------------------------------------------- 1 | services: 2 | redis: 3 | image: redis:5 -------------------------------------------------------------------------------- /compose/sidekiq.yml: -------------------------------------------------------------------------------- 1 | services: 2 | sidekiq: 3 | build: ../. 4 | env_file: ../.env 5 | command: bundle exec sidekiq -c 100 6 | restart: always 7 | volumes: 8 | - "../log:/app/log" 9 | environment: 10 | db_pool_size: 100 11 | logging: 12 | driver: "json-file" 13 | options: 14 | max-size: "100m" 15 | deploy: 16 | resources: 17 | limits: 18 | memory: 4G 19 | -------------------------------------------------------------------------------- /compose/telnet-task.yml: -------------------------------------------------------------------------------- 1 | services: 2 | telnet-task: 3 | build: ../. 4 | env_file: ../.env 5 | command: bundle exec rake telnet:push 6 | restart: always 7 | environment: 8 | db_pool_size: 2 9 | logging: 10 | driver: "json-file" 11 | options: 12 | max-size: "100m" 13 | -------------------------------------------------------------------------------- /compose/web.yml: -------------------------------------------------------------------------------- 1 | services: 2 | web: 3 | image: nginx 4 | depends_on: 5 | app: 6 | condition: service_healthy 7 | restart: true 8 | healthcheck: 9 | test: ["CMD-SHELL", "curl http://app:3000"] 10 | timeout: 10s 11 | restart: always 12 | ports: 13 | - 80:80 14 | - 80:80/udp 15 | - 443:443 16 | - 443:443/udp 17 | volumes: 18 | - ../public:/app/public 19 | - ../scripts/nginx-conf/api.smartcitizen.me.conf:/etc/nginx/conf.d/api.smartcitizen.me.conf 20 | - ../scripts/nginx.conf:/etc/nginx/nginx.conf 21 | - ../scripts/certs:/etc/ssl:ro 22 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | 5 | use Rack::JSONP 6 | run Rails.application 7 | -------------------------------------------------------------------------------- /config/banned_words.yml: -------------------------------------------------------------------------------- 1 | # replace with your own list 2 | 3 | - clashables: 4 | - map 5 | - about 6 | 7 | - profanities: 8 | - stupid 9 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | require 'bootsnap/setup' # Speed up boot time by caching expensive operations. 5 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /config/initializers/api_cache.rb: -------------------------------------------------------------------------------- 1 | APICache.store = Moneta.new(:Redis,{ 2 | cache: 10.minutes, # After this time fetch new data 3 | valid: 1.day, # Maximum time to use old data :forever is a valid option 4 | period: 1.minute, # Maximum frequency to call API 5 | timeout: 5.seconds # API response timeout 6 | # :fail => # Value returned instead of exception on failure 7 | }) 8 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 11 | # Rails.application.config.assets.precompile += %w( search.js ) 12 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /config/initializers/cors.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Avoid CORS issues when API is called from the frontend app. 4 | # Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests. 5 | 6 | # Read more: https://github.com/cyu/rack-cors 7 | 8 | # Rails.application.config.middleware.insert_before 0, Rack::Cors do 9 | # allow do 10 | # origins 'example.com' 11 | # 12 | # resource '*', 13 | # headers: :any, 14 | # methods: [:get, :post, :put, :patch, :delete, :options, :head] 15 | # end 16 | # end 17 | -------------------------------------------------------------------------------- /config/initializers/countries.rb: -------------------------------------------------------------------------------- 1 | ISO3166::Data.register( 2 | alpha2: 'XK', 3 | alpha3: 'XKX', 4 | name: 'Kosovo', 5 | continent: "Europe", 6 | currency_code: 'EUR', 7 | geo: { 8 | latitude: 42.602636, 9 | longitude: 20.902977, 10 | }, 11 | translations: { 12 | en: 'Kosovo' 13 | } 14 | ) -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /config/initializers/force_ssl.rb: -------------------------------------------------------------------------------- 1 | if Rails.application.config.force_ssl 2 | Rails.application.routes.default_url_options[:protocol] = "https" 3 | end -------------------------------------------------------------------------------- /config/initializers/git_info.rb: -------------------------------------------------------------------------------- 1 | GIT_REVISION = `git rev-parse --short HEAD`.chomp || 'revision not found' 2 | GIT_BRANCH = `git rev-parse --abbrev-ref HEAD`.chomp || 'branch not found' 3 | 4 | if File.exists?('VERSION') 5 | VERSION_FILE = `cat VERSION` 6 | else 7 | VERSION_FILE = 'VERSION file not found' 8 | end 9 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /config/initializers/json.rb: -------------------------------------------------------------------------------- 1 | MultiJson.use(:oj) 2 | ActiveSupport::JSON::Encoding.time_precision = 0 3 | 4 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /config/initializers/monkey_patches.rb: -------------------------------------------------------------------------------- 1 | class Float 2 | def round_to(x) 3 | (self * 10**x).round.to_f / 10**x 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/initializers/mysql.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | 3 | class MySQL < ActiveRecord::Base 4 | end 5 | 6 | # TODO: remove this file. This breaks the test because the lines above are needed! 7 | -------------------------------------------------------------------------------- /config/initializers/pg.rb: -------------------------------------------------------------------------------- 1 | PgSearch.multisearch_options = { 2 | :using => { :tsearch => {:prefix => true} }, 3 | :ignoring => :accents 4 | } 5 | -------------------------------------------------------------------------------- /config/initializers/premailer.rb: -------------------------------------------------------------------------------- 1 | class Premailer 2 | module Rails 3 | module CSSLoaders 4 | module AssetPipelineLoader 5 | extend self 6 | def asset_pipeline_present? 7 | false 8 | end 9 | end 10 | end 11 | end 12 | end -------------------------------------------------------------------------------- /config/initializers/rack_attack.rb: -------------------------------------------------------------------------------- 1 | class Rack::Attack 2 | class Request < ::Rack::Request 3 | def remote_ip 4 | @remote_ip ||= ActionDispatch::Request.new(env).remote_ip 5 | end 6 | end 7 | 8 | if Rails.env.development? 9 | # In environments/development.rb, config.cache_store = :null_store 10 | # Without a 'normal' cache it cannot count how many times a request has been made. 11 | # Instead we manually configure this cache for development mode: 12 | Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new 13 | end 14 | 15 | limit_proc = ->(req) { 16 | user_is_whitelisted = Rack::Attack.cache.read("throttle_whitelist_#{req.remote_ip}") 17 | user_is_whitelisted ? Float::INFINITY : ENV.fetch("THROTTLE_LIMIT", 150).to_i 18 | } 19 | 20 | throttle('Throttle by IP', limit: limit_proc, period: 1.minute) do |request| 21 | request.remote_ip 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /config/initializers/ransack.rb: -------------------------------------------------------------------------------- 1 | Ransack.configure do |config| 2 | # Raise errors if a query contains an unknown predicate or attribute. 3 | # Default is true (do not raise error on unknown conditions). 4 | config.ignore_unknown_conditions = false 5 | end -------------------------------------------------------------------------------- /config/initializers/request_cloudflare_ip.rb: -------------------------------------------------------------------------------- 1 | class ActionDispatch::Request 2 | alias_method :original_remote_ip, :remote_ip 3 | def remote_ip 4 | headers["HTTP_CF_CONNECTING_IP"] || original_remote_ip 5 | end 6 | end -------------------------------------------------------------------------------- /config/initializers/scheduler.rb: -------------------------------------------------------------------------------- 1 | # config/initializers/scheduler.rb 2 | require 'rufus-scheduler' 3 | 4 | # Let's use the rufus-scheduler singleton 5 | # 6 | s = Rufus::Scheduler.singleton 7 | 8 | 9 | # Only when NOT inside rake task or console 10 | return if defined?(Rails::Console) || Rails.env.development? || Rails.env.test? || File.split($0).last == 'rake' 11 | 12 | 13 | s.every '1m' do 14 | # debug 15 | #Rails.logger.info "hello, it's #{Time.now}" 16 | #Rails.logger.flush 17 | end 18 | 19 | s.every '1h' do 20 | CheckBatteryLevelBelowJob.perform_later 21 | CheckDeviceStoppedPublishingJob.perform_later 22 | end 23 | 24 | s.every '1d' do 25 | CheckupUserEmailBlankJob.perform_later 26 | DeleteArchivedDevicesJob.perform_later 27 | DeleteArchivedUsersJob.perform_later 28 | DeleteOrphanedDevicesJob.perform_later 29 | end 30 | -------------------------------------------------------------------------------- /config/initializers/sentry.rb: -------------------------------------------------------------------------------- 1 | Sentry.init do |config| 2 | config.dsn = ENV['RAVEN_DSN_URL'] 3 | config.breadcrumbs_logger = [:sentry_logger, :active_support_logger, :http_logger] 4 | config.excluded_exceptions = ["RetryMQTTMessageJob::RetryMessageHandlerError", 'ActionController::RoutingError', 'ActiveRecord::RecordNotFound'] 5 | end 6 | -------------------------------------------------------------------------------- /config/initializers/string.rb: -------------------------------------------------------------------------------- 1 | class String 2 | def numeric? 3 | Float(self) != nil rescue false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/initializers/timeout.rb: -------------------------------------------------------------------------------- 1 | #Rack::Timeout.timeout = 15 # seconds 2 | Rails.application.config.middleware.insert_before Rack::Runtime, Rack::Timeout, service_timeout: 20 3 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /config/locales/helpers/user/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | second_person_possessive: "your" 3 | third_person_possessive: "%{username}'s" 4 | first_person_possessive: "my" 5 | -------------------------------------------------------------------------------- /config/locales/views/layout/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | title: "Smart Citizen platform" 3 | -------------------------------------------------------------------------------- /config/locales/views/nav/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | logo_alt: "SmartCitizen" 3 | hamburger_label: "Toggle navigation" 4 | map_link_url: "https://smartcitizen.me/kits/" 5 | map_link_text: "Map" 6 | documentation_link_url: "https://docs.smartcitizen.me" 7 | documentation_link_text: "Documentation" 8 | forum_link_url: "https://forum.smartcitizen.me" 9 | forum_link_text: "Forum" 10 | api_link_url: "https://developer.smartcitizen.me" 11 | api_link_text: "API reference" 12 | about_link_url: "https://docs.smartcitizen.me/about/" 13 | about_link_text: "About" 14 | policy_link_url: "/ui/policy/" 15 | policy_link_text: "Policy" 16 | kit_link_url: "https://smartcitizen.me#get-your-kit" 17 | kit_link_text: "Get your kit" 18 | login_link_text: "Log in" 19 | profile_pic_alt_text: "Your profile" 20 | logout_link_text: "Log out" 21 | -------------------------------------------------------------------------------- /config/locales/views/sessions/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | new_session_submit: "Log in" 3 | new_account_heading: "Don't have an account?" 4 | new_account_submit: "Sign up" 5 | forgot_password_heading: "Forgot your password?" 6 | forgot_password_submit: "Reset password" 7 | -------------------------------------------------------------------------------- /config/locales/views/shared/copyable_input/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | copyable_input_button_title: Copy to clipboard 3 | -------------------------------------------------------------------------------- /config/locales/views/shared/danger_zone/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | danger_zone_subhead: Danger zone! 3 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: 424f949ea17a293558a0b72ec0f78b30b036495a24b0759b85dd017a5ac03d4dcb50e38032da14007ec248be8bd714d4f89a6754cc236bcd9a27c84109b19aac 15 | 16 | test: 17 | secret_key_base: 8d8bbd817204883412e5d9974101ccda616cd5a81589a1009ddf1734ab2fd7b32fd0b20bbf0bf9d74f5c022d8504a4a889e543e008155ad6b97c086bba18b1c0 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /config/sidekiq.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :verbose: false 3 | :concurrency: 10 4 | :queues: 5 | - default 6 | - mailers 7 | - mqtt_retry 8 | - mqtt_forward 9 | 10 | production: 11 | :concurrency: 20 12 | staging: 13 | :concurrency: 15 14 | -------------------------------------------------------------------------------- /config/webpack/development.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = process.env.NODE_ENV || 'development' 2 | 3 | const environment = require('./environment') 4 | 5 | module.exports = environment.toWebpackConfig() 6 | -------------------------------------------------------------------------------- /config/webpack/environment.js: -------------------------------------------------------------------------------- 1 | const { environment } = require('@rails/webpacker') 2 | 3 | module.exports = environment 4 | -------------------------------------------------------------------------------- /config/webpack/production.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = process.env.NODE_ENV || 'production' 2 | 3 | const environment = require('./environment') 4 | 5 | module.exports = environment.toWebpackConfig() 6 | -------------------------------------------------------------------------------- /config/webpack/test.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = process.env.NODE_ENV || 'development' 2 | 3 | const environment = require('./environment') 4 | 5 | module.exports = environment.toWebpackConfig() 6 | -------------------------------------------------------------------------------- /db/migrate/20150126131930_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration 2 | def change 3 | create_table :users do |t| 4 | t.string :first_name 5 | t.string :last_name 6 | t.string :username 7 | t.string :email 8 | t.string :password_digest 9 | 10 | t.timestamps null: false 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20150126155743_create_devices.rb: -------------------------------------------------------------------------------- 1 | class CreateDevices < ActiveRecord::Migration 2 | def change 3 | create_table :devices do |t| 4 | t.belongs_to :owner, index: true 5 | t.string :name 6 | t.text :description 7 | t.macaddr :mac_address 8 | t.float :latitude 9 | t.float :longitude 10 | t.timestamps null: false 11 | end 12 | add_foreign_key :devices, :users, column: :owner_id 13 | end 14 | end 15 | 16 | # DEVICE 17 | # owner_id 18 | # name 19 | # description 20 | # mac_address # add_column :devices, :mac_address, :macaddr 21 | # kit_version 22 | # firmware_version 23 | # elevation 24 | # latitude 25 | # longitude 26 | # address 27 | 28 | # READING 29 | # device_id 30 | -------------------------------------------------------------------------------- /db/migrate/20150128122757_create_api_tokens.rb: -------------------------------------------------------------------------------- 1 | class CreateApiTokens < ActiveRecord::Migration 2 | def change 3 | create_table :api_tokens do |t| 4 | t.belongs_to :owner, index: true, null: false 5 | t.string :token, null: false 6 | t.timestamps null: false 7 | end 8 | add_index :api_tokens, [:owner_id, :token], unique: true 9 | add_foreign_key :api_tokens, :users, column: :owner_id 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20150129141454_create_friendly_id_slugs.rb: -------------------------------------------------------------------------------- 1 | class CreateFriendlyIdSlugs < ActiveRecord::Migration 2 | def change 3 | create_table :friendly_id_slugs do |t| 4 | t.string :slug, :null => false 5 | t.integer :sluggable_id, :null => false 6 | t.string :sluggable_type, :limit => 50 7 | t.string :scope 8 | t.datetime :created_at 9 | end 10 | add_index :friendly_id_slugs, :sluggable_id 11 | add_index :friendly_id_slugs, [:slug, :sluggable_type] 12 | add_index :friendly_id_slugs, [:slug, :sluggable_type, :scope], :unique => true 13 | add_index :friendly_id_slugs, :sluggable_type 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20150129163302_create_sensors.rb: -------------------------------------------------------------------------------- 1 | class CreateSensors < ActiveRecord::Migration 2 | def change 3 | create_table :sensors do |t| 4 | t.string :ancestry, index: true 5 | t.string :name 6 | t.text :description 7 | t.string :unit 8 | 9 | t.timestamps null: false 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20150129170516_create_kits.rb: -------------------------------------------------------------------------------- 1 | class CreateKits < ActiveRecord::Migration 2 | def change 3 | create_table :kits do |t| 4 | t.string :name 5 | t.text :description 6 | 7 | t.timestamps null: false 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20150129170545_add_kit_id_to_devices.rb: -------------------------------------------------------------------------------- 1 | class AddKitIdToDevices < ActiveRecord::Migration 2 | def change 3 | add_reference :devices, :kit, index: true 4 | add_foreign_key :devices, :kits 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20150129170854_create_components.rb: -------------------------------------------------------------------------------- 1 | class CreateComponents < ActiveRecord::Migration 2 | def change 3 | create_table :components do |t| 4 | t.belongs_to :board, polymorphic: true, index: true 5 | t.belongs_to :sensor, index: true 6 | 7 | t.timestamps null: false 8 | end 9 | add_foreign_key :components, :sensors 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20150130104734_add_hstore_to_devices.rb: -------------------------------------------------------------------------------- 1 | class AddHstoreToDevices < ActiveRecord::Migration 2 | 3 | def up 4 | enable_extension :hstore 5 | end 6 | 7 | def down 8 | disable_extension :hstore 9 | end 10 | 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20150130104735_add_latest_data_to_devices.rb: -------------------------------------------------------------------------------- 1 | class AddLatestDataToDevices < ActiveRecord::Migration 2 | def up 3 | add_column :devices, :latest_data, :hstore 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150201170035_add_password_reset_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddPasswordResetToUsers < ActiveRecord::Migration 2 | def change 3 | add_column :users, :password_reset_token, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150202140127_add_geohash_to_devices.rb: -------------------------------------------------------------------------------- 1 | class AddGeohashToDevices < ActiveRecord::Migration 2 | def change 3 | add_column :devices, :geohash, :string 4 | add_index :devices, :geohash 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20150202154555_add_old_password_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddOldPasswordToUsers < ActiveRecord::Migration 2 | def change 3 | add_column :users, :old_password, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150204174044_add_last_recorded_at_to_devices.rb: -------------------------------------------------------------------------------- 1 | class AddLastRecordedAtToDevices < ActiveRecord::Migration 2 | def change 3 | add_column :devices, :last_recorded_at, :timestamp 4 | add_index :devices, :last_recorded_at 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20150204202307_add_slug_to_kits.rb: -------------------------------------------------------------------------------- 1 | class AddSlugToKits < ActiveRecord::Migration 2 | def change 3 | add_column :kits, :slug, :string 4 | add_index :kits, :slug 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20150211081350_add_fields_to_devices.rb: -------------------------------------------------------------------------------- 1 | class AddFieldsToDevices < ActiveRecord::Migration 2 | def change 3 | add_column :devices, :meta, :hstore 4 | add_column :devices, :location, :hstore 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20150211081539_add_fields_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddFieldsToUsers < ActiveRecord::Migration 2 | def change 3 | add_column :users, :role, :string 4 | add_column :users, :meta, :hstore 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20150313203310_add_location_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddLocationToUsers < ActiveRecord::Migration 2 | def change 3 | add_column :users, :city, :string 4 | add_column :users, :country_code, :string 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20150331102507_add_data_to_devices.rb: -------------------------------------------------------------------------------- 1 | class AddDataToDevices < ActiveRecord::Migration 2 | def change 3 | add_column :devices, :data, :jsonb 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150424150102_add_url_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddUrlToUsers < ActiveRecord::Migration 2 | def change 3 | add_column :users, :url, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150424155114_add_avatar_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddAvatarToUsers < ActiveRecord::Migration 2 | def change 3 | add_column :users, :avatar, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150428221051_create_pg_search_documents.rb: -------------------------------------------------------------------------------- 1 | class CreatePgSearchDocuments < ActiveRecord::Migration 2 | def self.up 3 | say_with_time("Creating table for pg_search multisearch") do 4 | create_table :pg_search_documents do |t| 5 | t.text :content 6 | t.belongs_to :searchable, :polymorphic => true, :index => true 7 | t.timestamps null: false 8 | end 9 | end 10 | end 11 | 12 | def self.down 13 | say_with_time("Dropping table for pg_search multisearch") do 14 | drop_table :pg_search_documents 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /db/migrate/20150514150525_add_old_data_to_devices.rb: -------------------------------------------------------------------------------- 1 | class AddOldDataToDevices < ActiveRecord::Migration 2 | def change 3 | add_column :devices, :old_data, :jsonb 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150514150744_add_trigger_to_devices.rb: -------------------------------------------------------------------------------- 1 | class AddTriggerToDevices < ActiveRecord::Migration 2 | def up 3 | execute <<-SQL 4 | CREATE FUNCTION replace_old_data() RETURNS trigger AS $$ 5 | BEGIN 6 | NEW.old_data := OLD.data; 7 | RETURN NEW; 8 | END; 9 | $$ language plpgsql; 10 | 11 | CREATE TRIGGER old_data_trigger 12 | BEFORE UPDATE 13 | ON devices 14 | FOR EACH ROW 15 | EXECUTE PROCEDURE replace_old_data(); 16 | SQL 17 | end 18 | 19 | def down 20 | execute <<-SQL 21 | DROP TRIGGER old_data_trigger ON devices; 22 | DROP FUNCTION replace_old_data(); 23 | SQL 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /db/migrate/20150520114511_add_owner_username_to_devices.rb: -------------------------------------------------------------------------------- 1 | class AddOwnerUsernameToDevices < ActiveRecord::Migration 2 | def change 3 | add_column :devices, :owner_username, :string 4 | Device.where.not(owner_id: nil).includes(:owner).each do |device| 5 | device.update_attribute(:owner_username, device.owner.username) 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20150602182642_create_pg_readings.rb: -------------------------------------------------------------------------------- 1 | class CreatePgReadings < ActiveRecord::Migration 2 | def change 3 | create_table :pg_readings do |t| 4 | t.belongs_to :device, index: true, foreign_key: true 5 | t.jsonb :data 6 | t.jsonb :raw_data 7 | t.datetime :recorded_at, index: true 8 | 9 | t.datetime :created_at 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20150701142525_create_measurements.rb: -------------------------------------------------------------------------------- 1 | class CreateMeasurements < ActiveRecord::Migration 2 | def change 3 | create_table :measurements do |t| 4 | t.string :name 5 | t.text :description 6 | t.string :unit 7 | 8 | t.timestamps null: false 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20150701142607_add_measurement_id_to_sensors.rb: -------------------------------------------------------------------------------- 1 | class AddMeasurementIdToSensors < ActiveRecord::Migration 2 | def change 3 | add_reference :sensors, :measurement, index: true, foreign_key: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150701190639_add_role_mask_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddRoleMaskToUsers < ActiveRecord::Migration 2 | def change 3 | add_column :users, :role_mask, :integer, default: 0, null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150702152151_enable_uuid_extension.rb: -------------------------------------------------------------------------------- 1 | class EnableUuidExtension < ActiveRecord::Migration 2 | def change 3 | enable_extension 'uuid-ossp' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150702152315_add_uuids_to_models.rb: -------------------------------------------------------------------------------- 1 | class AddUuidsToModels < ActiveRecord::Migration 2 | def change 3 | [:components, :devices, :kits, :measurements, :sensors, :users].each do |model| 4 | add_column model, :uuid, :uuid, default: 'uuid_generate_v4()', null: false 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20150716090911_create_uploads.rb: -------------------------------------------------------------------------------- 1 | class CreateUploads < ActiveRecord::Migration 2 | def change 3 | create_table :uploads do |t| 4 | t.string :type 5 | t.string :original_filename 6 | t.jsonb :metadata 7 | 8 | t.timestamps null: false 9 | end 10 | add_index :uploads, :type 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20150721114116_add_legacy_api_key_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddLegacyApiKeyToUsers < ActiveRecord::Migration 2 | def change 3 | add_column :users, :legacy_api_key, :string 4 | User.reset_column_information 5 | User.all.each do |u| 6 | u.update_attribute(:legacy_api_key, Digest::SHA1.hexdigest("#{SecureRandom.uuid}#{rand(1000)}".split("").shuffle.join) ) 7 | end 8 | change_column :users, :legacy_api_key, :string, null: false 9 | add_index :users, :legacy_api_key, unique: true 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20150722141027_add_old_data_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddOldDataToUsers < ActiveRecord::Migration 2 | def change 3 | add_column :users, :old_data, :jsonb 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150723151339_drop_unneeded_user_fields.rb: -------------------------------------------------------------------------------- 1 | class DropUnneededUserFields < ActiveRecord::Migration 2 | def change 3 | remove_column :users, :first_name, :string 4 | remove_column :users, :last_name, :string 5 | remove_column :users, :old_password, :string 6 | remove_column :users, :role, :string 7 | remove_column :users, :meta, :hstore 8 | rename_column :users, :avatar, :avatar_url 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20150727075855_add_uuids_to_uploads.rb: -------------------------------------------------------------------------------- 1 | class AddUuidsToUploads < ActiveRecord::Migration 2 | def change 3 | add_column :uploads, :uuid, :uuid, default: 'uuid_generate_v4()', null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150727121643_add_user_id_to_uploads.rb: -------------------------------------------------------------------------------- 1 | class AddUserIdToUploads < ActiveRecord::Migration 2 | def change 3 | add_reference :uploads, :user, index: true, foreign_key: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150727150738_add_key_to_uploads.rb: -------------------------------------------------------------------------------- 1 | class AddKeyToUploads < ActiveRecord::Migration 2 | def change 3 | add_column :uploads, :key, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150728112029_drop_fk_restraint_on_devices.rb: -------------------------------------------------------------------------------- 1 | class DropFkRestraintOnDevices < ActiveRecord::Migration 2 | def change 3 | remove_foreign_key :devices, column: :owner_id 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150731101949_fix_pks.rb: -------------------------------------------------------------------------------- 1 | class FixPks < ActiveRecord::Migration 2 | def change 3 | ActiveRecord::Base.connection.tables.each do |t| 4 | ActiveRecord::Base.connection.reset_pk_sequence!(t) 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20150806215427_add_trigrams.rb: -------------------------------------------------------------------------------- 1 | class AddTrigrams < ActiveRecord::Migration 2 | def change 3 | enable_extension "pg_trgm" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150806215704_add_unaccent.rb: -------------------------------------------------------------------------------- 1 | class AddUnaccent < ActiveRecord::Migration 2 | def change 3 | enable_extension "unaccent" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150813072846_add_migration_data_to_devices.rb: -------------------------------------------------------------------------------- 1 | class AddMigrationDataToDevices < ActiveRecord::Migration 2 | def change 3 | add_column :devices, :migration_data, :jsonb 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150825103243_create_places.rb: -------------------------------------------------------------------------------- 1 | class CreatePlaces < ActiveRecord::Migration 2 | def change 3 | create_table :places do |t| 4 | t.string :name 5 | t.string :country_code 6 | t.string :country_name 7 | t.float :lat 8 | t.float :lng 9 | 10 | t.timestamps null: false 11 | end 12 | add_index :places, [:name, :country_code], unique: true 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/20150827133315_add_workflow_state_to_devices.rb: -------------------------------------------------------------------------------- 1 | class AddWorkflowStateToDevices < ActiveRecord::Migration 2 | def change 3 | add_column :devices, :workflow_state, :string 4 | add_index :devices, :workflow_state 5 | Device.reset_column_information 6 | Device.unscoped.update_all(workflow_state: 'active') 7 | # Device.all.each do |device| 8 | # device.update_attributes(:workflow_state => 'normal') 9 | # end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20150907223941_create_tags.rb: -------------------------------------------------------------------------------- 1 | class CreateTags < ActiveRecord::Migration 2 | def change 3 | create_table :tags do |t| 4 | t.string :name 5 | t.text :description 6 | t.timestamps null: false 7 | end 8 | add_index :tags, :name, unique: true 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20150907232654_create_devices_tags.rb: -------------------------------------------------------------------------------- 1 | class CreateDevicesTags < ActiveRecord::Migration 2 | def change 3 | create_table :devices_tags do |t| 4 | t.belongs_to :device, foreign_key: true 5 | t.belongs_to :tag, foreign_key: true 6 | t.timestamps null: false 7 | end 8 | 9 | add_index :devices_tags, [:device_id, :tag_id], unique: true 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20150912192902_add_uuid_to_tags.rb: -------------------------------------------------------------------------------- 1 | class AddUuidToTags < ActiveRecord::Migration 2 | def change 3 | add_column :tags, :uuid, :uuid, default: 'uuid_generate_v4()', null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150916160713_add_equation_to_sensors.rb: -------------------------------------------------------------------------------- 1 | class AddEquationToSensors < ActiveRecord::Migration 2 | def change 3 | add_column :sensors, :equation, :text 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150916163343_add_sensor_map_to_kits.rb: -------------------------------------------------------------------------------- 1 | class AddSensorMapToKits < ActiveRecord::Migration 2 | def change 3 | add_column :kits, :sensor_map, :jsonb 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150917235839_add_equation_to_components.rb: -------------------------------------------------------------------------------- 1 | class AddEquationToComponents < ActiveRecord::Migration 2 | def change 3 | add_column :components, :equation, :text 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150918012211_drop_equation_from_sensors.rb: -------------------------------------------------------------------------------- 1 | class DropEquationFromSensors < ActiveRecord::Migration 2 | def change 3 | remove_column :sensors, :equation, :text 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150920212633_add_cached_device_ids_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddCachedDeviceIdsToUsers < ActiveRecord::Migration 2 | def change 3 | add_column :users, :cached_device_ids, :integer, array: true 4 | User.reset_column_information 5 | 6 | User.unscoped.all.map(&:update_all_device_ids!) 7 | 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20151007152201_create_bad_readings.rb: -------------------------------------------------------------------------------- 1 | class CreateBadReadings < ActiveRecord::Migration 2 | def change 3 | create_table :bad_readings do |t| 4 | t.integer :tags 5 | t.string :remote_ip 6 | t.jsonb :data, null: false 7 | t.datetime :created_at, null: false 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20151007191504_drop_pg_readings.rb: -------------------------------------------------------------------------------- 1 | class DropPgReadings < ActiveRecord::Migration 2 | def change 3 | drop_table :pg_readings 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20151029153355_add_csv_export_requested_at_to_devices.rb: -------------------------------------------------------------------------------- 1 | class AddCsvExportRequestedAtToDevices < ActiveRecord::Migration 2 | def change 3 | add_column :devices, :csv_export_requested_at, :datetime 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20151113111350_change_hstore_fields_to_jsonb_on_devices.rb: -------------------------------------------------------------------------------- 1 | class ChangeHstoreFieldsToJsonbOnDevices < ActiveRecord::Migration 2 | def up 3 | change_column :devices, :meta, 'jsonb USING CAST(meta AS jsonb)' 4 | change_column :devices, :location, 'jsonb USING CAST(location AS jsonb)' 5 | end 6 | 7 | def down 8 | puts("****************** Data Migration Warning ******************".red) 9 | puts("This will WIPE meta and location data".yellow) 10 | puts("press 'y' if you wish to continue".yellow) 11 | 12 | if STDIN.gets.chomp == "y" 13 | puts("Ok then!".green) 14 | else 15 | fail 16 | end 17 | 18 | remove_column :devices, :meta 19 | remove_column :devices, :location 20 | add_column :devices, :meta, :hstore 21 | add_column :devices, :location, :hstore 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /db/migrate/20151117190908_add_message_to_bad_readings.rb: -------------------------------------------------------------------------------- 1 | class AddMessageToBadReadings < ActiveRecord::Migration 2 | def change 3 | add_column :bad_readings, :message, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20151117194000_add_device_id_and_mac_address_to_bad_readings.rb: -------------------------------------------------------------------------------- 1 | class AddDeviceIdAndMacAddressToBadReadings < ActiveRecord::Migration 2 | def change 3 | add_column :bad_readings, :device_id, :integer 4 | add_column :bad_readings, :mac_address, :string 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20151117194820_add_version_to_bad_readings.rb: -------------------------------------------------------------------------------- 1 | class AddVersionToBadReadings < ActiveRecord::Migration 2 | def change 3 | add_column :bad_readings, :version, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20151117200126_add_timestamp_to_bad_readings.rb: -------------------------------------------------------------------------------- 1 | class AddTimestampToBadReadings < ActiveRecord::Migration 2 | def change 3 | add_column :bad_readings, :timestamp, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20151118083900_add_backtrace_to_bad_readings.rb: -------------------------------------------------------------------------------- 1 | class AddBacktraceToBadReadings < ActiveRecord::Migration 2 | def change 3 | add_column :bad_readings, :backtrace, :text 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20151118143226_create_backup_readings.rb: -------------------------------------------------------------------------------- 1 | class CreateBackupReadings < ActiveRecord::Migration 2 | def change 3 | create_table :backup_readings do |t| 4 | t.jsonb :data 5 | t.string :mac 6 | t.string :version 7 | t.string :ip 8 | t.boolean :stored 9 | t.datetime :created_at 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20151209135345_add_workflow_state_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddWorkflowStateToUsers < ActiveRecord::Migration 2 | def change 3 | add_column :users, :workflow_state, :string 4 | add_index :users, :workflow_state 5 | User.update_all(workflow_state: 'active') 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20160313185543_add_old_mac_address_to_devices.rb: -------------------------------------------------------------------------------- 1 | class AddOldMacAddressToDevices < ActiveRecord::Migration 2 | def change 3 | add_column :devices, :old_mac_address, :macaddr 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20160404101737_drop_places.rb: -------------------------------------------------------------------------------- 1 | class DropPlaces < ActiveRecord::Migration 2 | 3 | def up 4 | drop_table :places 5 | end 6 | 7 | def down 8 | create_table "places", force: :cascade do |t| 9 | t.string "name" 10 | t.string "country_code" 11 | t.string "country_name" 12 | t.float "lat" 13 | t.float "lng" 14 | t.datetime "created_at", null: false 15 | t.datetime "updated_at", null: false 16 | end 17 | 18 | add_index "places", ["name", "country_code"], name: "index_places_on_name_and_country_code", unique: true, using: :btree 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /db/migrate/20160411194100_add_owner_to_application.rb: -------------------------------------------------------------------------------- 1 | class AddOwnerToApplication < ActiveRecord::Migration 2 | def change 3 | add_column :oauth_applications, :owner_id, :integer, null: true 4 | add_column :oauth_applications, :owner_type, :string, null: true 5 | add_index :oauth_applications, [:owner_id, :owner_type] 6 | end 7 | end -------------------------------------------------------------------------------- /db/migrate/20160601120221_add_reverse_equation_to_components.rb: -------------------------------------------------------------------------------- 1 | class AddReverseEquationToComponents < ActiveRecord::Migration 2 | def change 3 | add_column :components, :reverse_equation, :text 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20160607101112_add_state_to_devices.rb: -------------------------------------------------------------------------------- 1 | class AddStateToDevices < ActiveRecord::Migration 2 | def change 3 | add_column :devices, :state, :string 4 | add_index :devices, :state 5 | 6 | Device.all.each do |d| 7 | d.update_column(:state, d.soft_state) 8 | end 9 | 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20161026171920_create_orphan_devices.rb: -------------------------------------------------------------------------------- 1 | class CreateOrphanDevices < ActiveRecord::Migration 2 | def change 3 | create_table :orphan_devices do |t| 4 | t.string :name 5 | t.text :description 6 | t.integer :kit_id 7 | t.string :exposure 8 | t.float :latitude 9 | t.float :longitude 10 | t.text :user_tags 11 | t.string :device_token, null: false 12 | t.string :onboarding_session 13 | 14 | t.timestamps null: false 15 | end 16 | 17 | add_index :orphan_devices, [:device_token], unique: true 18 | 19 | add_column :devices, :device_token, :string 20 | add_index :devices, [:device_token], unique: true 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /db/migrate/20180719113808_create_devices_inventory.rb: -------------------------------------------------------------------------------- 1 | class CreateDevicesInventory < ActiveRecord::Migration 2 | def change 3 | drop_table :devices_inventory 4 | create_table :devices_inventory do |t| 5 | t.jsonb :report, default: '{}' 6 | t.datetime :created_at 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20180903161334_add_confidential_to_doorkeeper_application.rb: -------------------------------------------------------------------------------- 1 | class AddConfidentialToDoorkeeperApplication < ActiveRecord::Migration 2 | def change 3 | add_column( 4 | :oauth_applications, 5 | :confidential, 6 | :boolean, 7 | null: false, 8 | default: true # maintaining backwards compatibility: require secrets 9 | ) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20181011105328_drop_bad_readings.rb: -------------------------------------------------------------------------------- 1 | class DropBadReadings < ActiveRecord::Migration 2 | def change 3 | drop_table :bad_readings 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20181011105345_drop_backup_readings.rb: -------------------------------------------------------------------------------- 1 | class DropBackupReadings < ActiveRecord::Migration 2 | def change 3 | drop_table :backup_readings 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20181105160320_create_sensor_tags.rb: -------------------------------------------------------------------------------- 1 | class CreateSensorTags < ActiveRecord::Migration 2 | def change 3 | create_table :tag_sensors do |t| 4 | t.string :name 5 | t.string :description 6 | 7 | t.timestamps null: false 8 | end 9 | 10 | create_table :sensor_tags do |t| 11 | t.timestamps null: false 12 | t.belongs_to :sensor, index: true 13 | t.belongs_to :tag_sensor, index: true 14 | end 15 | 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /db/migrate/20181212142627_add_hardware_info_to_devices.rb: -------------------------------------------------------------------------------- 1 | class AddHardwareInfoToDevices < ActiveRecord::Migration[5.2] 2 | def change 3 | add_column :devices, :hardware_info, :jsonb 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20190116161536_add_notifications_to_devices.rb: -------------------------------------------------------------------------------- 1 | class AddNotificationsToDevices < ActiveRecord::Migration[5.2] 2 | def change 3 | add_column :devices, :notify_stopped_publishing_timestamp, :timestamp, :default => Time.now 4 | add_column :devices, :notify_low_battery_timestamp, :timestamp, :default => Time.now 5 | add_column :devices, :notify_low_battery, :boolean, default: false 6 | add_column :devices, :notify_stopped_publishing, :boolean,default: false 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20190222130041_add_device_handshake_to_orphan_devices.rb: -------------------------------------------------------------------------------- 1 | class AddDeviceHandshakeToOrphanDevices < ActiveRecord::Migration[5.2] 2 | def change 3 | add_column :orphan_devices, :device_handshake, :boolean, default: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20190819084816_add_is_private_to_devices.rb: -------------------------------------------------------------------------------- 1 | class AddIsPrivateToDevices < ActiveRecord::Migration[5.2] 2 | def change 3 | add_column :devices, :is_private, :boolean, default: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20191115103215_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.active_storage.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from active_storage (originally 20180723000244) 2 | class AddForeignKeyConstraintToActiveStorageAttachmentsForBlobId < ActiveRecord::Migration[6.0] 3 | def up 4 | return if foreign_key_exists?(:active_storage_attachments, column: :blob_id) 5 | 6 | if table_exists?(:active_storage_blobs) 7 | add_foreign_key :active_storage_attachments, :active_storage_blobs, column: :blob_id 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20200703144927_add_post_processing_info_to_device.rb: -------------------------------------------------------------------------------- 1 | class AddPostProcessingInfoToDevice < ActiveRecord::Migration[6.0] 2 | def change 3 | add_column :devices, :postprocessing_info, :jsonb 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20210105123052_add_is_test_to_devices.rb: -------------------------------------------------------------------------------- 1 | class AddIsTestToDevices < ActiveRecord::Migration[6.0] 2 | def change 3 | add_column :devices, :is_test, :boolean, null: false, default: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20210204124227_create_postprocessings.rb: -------------------------------------------------------------------------------- 1 | class CreatePostprocessings < ActiveRecord::Migration[6.0] 2 | def change 3 | remove_column :devices, :postprocessing_info, :jsonb 4 | 5 | create_table :postprocessings do |t| 6 | t.string :blueprint_url 7 | t.string :hardware_url 8 | t.belongs_to :device, null: false, foreign_key: true 9 | t.jsonb :forwarding_params 10 | t.jsonb :meta 11 | t.datetime :latest_postprocessing 12 | 13 | t.timestamps 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /db/migrate/20230512075843_add_archived_at_to_devices.rb: -------------------------------------------------------------------------------- 1 | class AddArchivedAtToDevices < ActiveRecord::Migration[6.0] 2 | def up 3 | add_column :devices, :archived_at, :datetime, null: true 4 | execute %{ 5 | UPDATE devices 6 | SET archived_at = NOW() 7 | WHERE state = 'archived' 8 | } 9 | end 10 | 11 | def down 12 | remove_column :devices, :archived_at 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/20230616151554_add_service_name_to_active_storage_blobs.active_storage.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from active_storage (originally 20190112182829) 2 | class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0] 3 | def up 4 | return unless table_exists?(:active_storage_blobs) 5 | 6 | unless column_exists?(:active_storage_blobs, :service_name) 7 | add_column :active_storage_blobs, :service_name, :string 8 | 9 | if configured_service = ActiveStorage::Blob.service.name 10 | ActiveStorage::Blob.unscoped.update_all(service_name: configured_service) 11 | end 12 | 13 | change_column :active_storage_blobs, :service_name, :string, null: false 14 | end 15 | end 16 | 17 | def down 18 | return unless table_exists?(:active_storage_blobs) 19 | 20 | remove_column :active_storage_blobs, :service_name 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /db/migrate/20230705095430_populate_device_archived_at_column.rb: -------------------------------------------------------------------------------- 1 | class PopulateDeviceArchivedAtColumn < ActiveRecord::Migration[6.0] 2 | def change 3 | execute %{ 4 | UPDATE devices 5 | SET archived_at = NOW() 6 | WHERE workflow_state = 'archived' 7 | } 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20230929114837_further_kits_refactor_changes.rb: -------------------------------------------------------------------------------- 1 | class FurtherKitsRefactorChanges < ActiveRecord::Migration[6.1] 2 | def change 3 | rename_column :devices, :last_recorded_at, :last_reading_at 4 | add_column :components, :location, :integer, default: 1 5 | connection.execute(%{ 6 | UPDATE components 7 | SET location=1 8 | WHERE location IS NULL 9 | }) 10 | change_column_null :components, :location, false 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20231005153412_rename_component_location_to_bus.rb: -------------------------------------------------------------------------------- 1 | class RenameComponentLocationToBus < ActiveRecord::Migration[6.1] 2 | def change 3 | rename_column :components, :location, :bus 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20231006064514_add_last_reading_at_to_components.rb: -------------------------------------------------------------------------------- 1 | class AddLastReadingAtToComponents < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :components, :last_reading_at, :datetime 4 | execute %{ 5 | UPDATE components 6 | SET last_reading_at = devices.last_reading_at 7 | FROM devices 8 | WHERE components.device_id = devices.id 9 | } 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20240228125910_remove_hardware_description_override_from_devices.rb: -------------------------------------------------------------------------------- 1 | class RemoveHardwareDescriptionOverrideFromDevices < ActiveRecord::Migration[6.1] 2 | def change 3 | remove_column :devices, :hardware_description_override, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20240318110256_add_world_map_indexes.rb: -------------------------------------------------------------------------------- 1 | class AddWorldMapIndexes < ActiveRecord::Migration[6.1] 2 | def change 3 | add_index :devices, [:workflow_state, :is_test, :last_reading_at, :latitude], name: "world_map_request" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20240318171656_add_component_device_sensor_index.rb: -------------------------------------------------------------------------------- 1 | class AddComponentDeviceSensorIndex < ActiveRecord::Migration[6.1] 2 | def change 3 | remove_index :components, [:sensor_id] 4 | add_index :components, [:device_id, :sensor_id] 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20240423162838_add_forwarding_token_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddForwardingTokenToUsers < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :users, :forwarding_token, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20240512132257_add_forwarding_username_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddForwardingUsernameToUsers < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :users, :forwarding_username, :string 4 | add_index :users, :forwarding_token 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20240624161155_add_data_policy_fields_to_device.rb: -------------------------------------------------------------------------------- 1 | class AddDataPolicyFieldsToDevice < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :devices, :precise_location, :boolean, null: false, default: false 4 | add_column :devices, :enable_forwarding, :boolean, null: false, default: false 5 | # Existing devices have precise locations, despite the default for all new ones. 6 | execute "UPDATE devices SET precise_location = true" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20240624175242_add_extra_measurement_and_sensor_fields.rb: -------------------------------------------------------------------------------- 1 | class AddExtraMeasurementAndSensorFields < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :measurements, :definition, :string 4 | add_column :sensors, :datasheet, :string 5 | add_column :sensors, :unit_definition, :string 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20240704062854_remove_avatars_and_uploads.rb: -------------------------------------------------------------------------------- 1 | class RemoveAvatarsAndUploads < ActiveRecord::Migration[6.1] 2 | def change 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /db/migrate/20240718054447_create_experiments.rb: -------------------------------------------------------------------------------- 1 | class CreateExperiments < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :experiments do |t| 4 | t.string :name, null: false 5 | t.string :description 6 | t.belongs_to :owner, index: true 7 | t.boolean :active, null: false, default: true 8 | t.boolean :is_test, null: false, default: false 9 | t.datetime :starts_at 10 | t.datetime :ends_at 11 | t.timestamps 12 | end 13 | add_foreign_key :experiments, :users, column: :owner_id 14 | 15 | create_table :devices_experiments, id: false do |t| 16 | t.belongs_to :device, index: true 17 | t.belongs_to :experiment, index: true 18 | end 19 | add_foreign_key :devices_experiments, :devices, column: :device_id 20 | add_foreign_key :devices_experiments, :experiments, column: :experiment_id 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /db/migrate/20240812081108_remove_active_flag_from_experiments.rb: -------------------------------------------------------------------------------- 1 | class RemoveActiveFlagFromExperiments < ActiveRecord::Migration[6.1] 2 | def change 3 | remove_column :experiments, :active, :boolean 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20241001080033_change_device_precise_location_default.rb: -------------------------------------------------------------------------------- 1 | class ChangeDevicePreciseLocationDefault < ActiveRecord::Migration[6.1] 2 | def change 3 | change_column_default :devices, :precise_location, from: false, to: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20241009174732_unique_index_on_components.rb: -------------------------------------------------------------------------------- 1 | class UniqueIndexOnComponents < ActiveRecord::Migration[6.1] 2 | def up 3 | remove_index :components, [:device_id, :sensor_id] 4 | add_index :components, [:device_id, :sensor_id], unique: true 5 | execute %{ 6 | ALTER TABLE components ADD CONSTRAINT unique_sensor_for_device UNIQUE (device_id, sensor_id) 7 | } 8 | execute %{ 9 | ALTER TABLE components ADD CONSTRAINT unique_key_for_device UNIQUE (device_id, key) 10 | } 11 | end 12 | 13 | def down 14 | execute %{ 15 | ALTER TABLE components DROP CONSTRAINT IF EXISTS unique_key_for_device 16 | } 17 | execute %{ 18 | ALTER TABLE components DROP CONSTRAINT IF EXISTS unique_sensor_for_device 19 | } 20 | remove_index :components, [:device_id, :sensor_id], unique: true 21 | add_index :components, [:device_id, :sensor_id] 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /db/migrate/20241014052837_create_device_ingest_errors.rb: -------------------------------------------------------------------------------- 1 | class CreateDeviceIngestErrors < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :ingest_errors do |t| 4 | t.references :device, null: false, foreign_key: true 5 | t.text :topic 6 | t.text :message 7 | t.text :error_class 8 | t.text :error_message 9 | t.text :error_trace 10 | t.timestamps 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20241113155952_remove_null_strings_from_measurement_units.rb: -------------------------------------------------------------------------------- 1 | class RemoveNullStringsFromMeasurementUnits < ActiveRecord::Migration[6.1] 2 | def up 3 | execute "UPDATE measurements SET unit = NULL WHERE unit = 'NULL'" 4 | execute "UPDATE sensors SET unit = NULL WHERE unit = 'NULL'" 5 | end 6 | 7 | def down; end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20250505081245_make_users_with_postprocessings_into_researchers.rb: -------------------------------------------------------------------------------- 1 | class MakeUsersWithPostprocessingsIntoResearchers < ActiveRecord::Migration[6.1] 2 | def change 3 | execute %{ 4 | UPDATE users 5 | SET role_mask = 2 6 | FROM devices 7 | INNER JOIN postprocessings on devices.id = postprocessings.device_id 8 | WHERE devices.owner_id = users.id AND postprocessings.id IS NOT NULL and users.role_mask = 0; 9 | } 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #git stash 3 | #git pull --rebase 4 | #git stash pop 5 | docker compose build 6 | docker compose exec app bundle exec bin/rake db:migrate 7 | docker compose exec app bash -l -c "bundle exec yarn install" 8 | docker compose exec app bash -l -c "bundle exec bin/rake assets:clobber" 9 | docker compose exec app bash -l -c "NODE_OPTIONS=--openssl-legacy-provider bundle exec bin/rake assets:precompile" 10 | docker compose up -d 11 | -------------------------------------------------------------------------------- /docs/adr/0000-use-markdown-architectural-decision-records.md: -------------------------------------------------------------------------------- 1 | # Use Markdown Architectural Decision Records 2 | 3 | - Status: trial 4 | - Deciders: viktorsmari 5 | - Date: 2020-12-03 6 | 7 | ## Why? 8 | 9 | Sometimes we forget why we made architectural changes, and finding the correct git commit which explains the 'why' can be difficult. 10 | -------------------------------------------------------------------------------- /docs/adr/0001-minimize-kit-payload.md: -------------------------------------------------------------------------------- 1 | - Deciders: vicobarberan 2 | - Date: 2020-12-03 3 | 4 | ## Why? 5 | 6 | - Try do reduce the payload that gets sent from a device to the API by ~35%. 7 | - Instead, the API will convert the new data type into JSON. 8 | 9 | 10 | New payload example: 11 | - https://github.com/fablabbcn/smartcitizen-kit-21/commit/f195dbc010c8cddc419cb4357875c9de942aab48#diff-f978f2d74f7bc8854e6bb019c93369fa30b05808be6de85f5571b5cc804db18fR414 12 | 13 | 14 | ``` 15 | { 16 | t:2017-03-24T13:35:14Z, 17 | 29:48.45, 18 | 13:66, 19 | 12:28, 20 | 10:4.45 21 | } 22 | ``` 23 | 24 | Old payload: 25 | - https://github.com/fablabbcn/smartcitizen-kit-21/blob/master/esp/src/SckESP.cpp#L361-L373 26 | 27 | ```json 28 | { "data":[ 29 | {"recorded_at":"2017-03-24T13:35:14Z", 30 | "sensors":[ 31 | {"id":29,"value":48.45}, 32 | {"id":13,"value":66}, 33 | {"id":12,"value":28}, 34 | {"id":10,"value":4.45} 35 | ] 36 | } 37 | ] 38 | } 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/adr/0002-private-devices.md: -------------------------------------------------------------------------------- 1 | - Deciders: pral2a 2 | - Date: 2019-09-01 3 | 4 | ## Problem 5 | 6 | - Some devices might contain data you don't want to be public. 7 | - Example: How often / when you turn on the lights in your bathroom? 8 | 9 | ## Solution 10 | 11 | - Add a boolean field `is_private` to devices, that can be used to hide their data. 12 | 13 | Who can see a private device data? 14 | - The data is only visible to the owner + admins 15 | 16 | What about the World Map? 17 | - The device name + location will be visible to EVERYONE on the World Map, but not it's data. 18 | -------------------------------------------------------------------------------- /docs/adr/0003-hide-test-devices.md: -------------------------------------------------------------------------------- 1 | - Deciders: pral2a oscgonfer 2 | - Date: 2021-01-04 3 | 4 | ## Problem 5 | 6 | - We have multiple 'test' devices that we create internally while developing features / testing sensors etc. 7 | - We don't want these devices to show up on the World Map, because they are clutter / noise. 8 | - The `is_test` field can also be used to quickly delete all test devices. 9 | 10 | ## Solution 11 | 12 | - Add a boolean field `is_test` (or similar) to devices, that we can activate in order to hide devices. 13 | 14 | 15 | ## Thoughts 16 | 17 | - Should our users also be able to do this themselves on their own devices? 18 | - Yes, but only users with ADMIN or RESEARCHER rights. 19 | - We have the `is_private` but that shows devices on the World Map. Can we change is_private to also hide devices on the world_map? 20 | -------------------------------------------------------------------------------- /docs/adr/README.md: -------------------------------------------------------------------------------- 1 | ## We are trying to use ADR - in order to document WHY we make decisions. 2 | 3 | Inspired by: 4 | 5 | - https://adr.github.io/ 6 | - https://github.com/joelparkerhenderson/architecture_decision_record 7 | - https://github.com/island-is/handbook/blob/master/docs/adr/ 8 | -------------------------------------------------------------------------------- /docs/adr/current-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fablabbcn/smartcitizen-api/cfd1394f9efd0bf49cba3b30b40b4e326d32bc7b/docs/adr/current-architecture.png -------------------------------------------------------------------------------- /docs/erd.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fablabbcn/smartcitizen-api/cfd1394f9efd0bf49cba3b30b40b4e326d32bc7b/docs/erd.pdf -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | You are here: http://fablabbcn.github.io/smartcitizen-api/ 2 | 3 | This is the index.md file from /docs folder, in the repository. 4 | 5 | [Architectural Decision Records](adr) 6 | 7 | [CSV_upload](CSV_upload.md) 8 | 9 | [mqtt_device_readings](mqtt_device_readings.md) 10 | 11 | [mqtt_handler](mqtt_handler.md) 12 | 13 | [onboarding](onboarding) 14 | -------------------------------------------------------------------------------- /docs/throttling.md: -------------------------------------------------------------------------------- 1 | Throttling is done as follows: 2 | - If the request is unauthenticated or the authorisation level doesn't allow it, `role_mask == 1`, the request is throttled at a rate of 150 requests/ minute 3 | - If the request is authorised with a `role_mask >= 2` (`researcher` or `admin`), the request is not throttled and it will do so based on the request remote ip for 5' 4 | 5 | This implies that if `role_mask>=2` requests are followed by unauthenticated or unauthorised requests from the same IP, those requests will not be throttled - we consider this a borderline case. 6 | Also, if an IP is throttled - returning 429s, a request `role_mask>=2` coming from that IP will get a 429 until 1' passes. 7 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | source $NVM_DIR/nvm.sh 4 | 5 | # Remove a potentially pre-existing server.pid for Rails. 6 | rm -f /app/tmp/pids/server.pid 7 | 8 | 9 | # Then exec the container's main process (what's set as CMD in the Dockerfile). 10 | exec "$@" 11 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fablabbcn/smartcitizen-api/cfd1394f9efd0bf49cba3b30b40b4e326d32bc7b/lib/tasks/.keep -------------------------------------------------------------------------------- /lib/tasks/devices.rake: -------------------------------------------------------------------------------- 1 | namespace :devices do 2 | task :truncate_and_fuzz_locations => :environment do 3 | Device.all.each do |device| 4 | device.truncate_and_fuzz_location! 5 | device.save!(validate: false) 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/tasks/postgres.rake: -------------------------------------------------------------------------------- 1 | namespace :postgres do 2 | desc 'Resets Postgres auto-increment ID column sequences to fix duplicate ID errors' 3 | task :reset_sequences => :environment do 4 | Rails.application.eager_load! 5 | 6 | ActiveRecord::Base.connection.tables.each do |model| 7 | begin 8 | ActiveRecord::Base.connection.reset_pk_sequence!(model) 9 | puts "reset #{model} sequence" 10 | rescue => e 11 | Rails.logger.error "Error resetting #{model} sequence: #{e.class.name}/#{e.message}" 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fablabbcn/smartcitizen-api/cfd1394f9efd0bf49cba3b30b40b4e326d32bc7b/log/.keep -------------------------------------------------------------------------------- /mqtt_subscriber.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | bundle exec rake mqtt:sub 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "browserslist": [ 3 | "defaults", 4 | "IE 11" 5 | ], 6 | "dependencies": { 7 | "@popperjs/core": "^2.11.8", 8 | "@rails/activestorage": "^8.0.100", 9 | "@rails/ujs": "^7.1.3-4", 10 | "@rails/webpacker": "5.4.4", 11 | "autocompleter": "^9.3.2", 12 | "bootstrap": "^5.3.3", 13 | "bootstrap5-tags": "^1.7.6", 14 | "d3": "^7.9.0", 15 | "flatpickr": "^4.6.13", 16 | "jquery": "^3.7.1", 17 | "leaflet": "^1.9.4", 18 | "leaflet-defaulticon-compatibility": "^0.1.2", 19 | "sorted-btree": "^1.8.1", 20 | "strftime": "^0.10.3", 21 | "webpack": "^4.46.0", 22 | "webpack-cli": "^3.3.12" 23 | }, 24 | "devDependencies": { 25 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11", 26 | "webpack-dev-server": "^3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('postcss-import'), 4 | require('postcss-flexbugs-fixes'), 5 | require('postcss-preset-env')({ 6 | autoprefixer: { 7 | flexbox: 'no-2009' 8 | }, 9 | stage: 3 10 | }) 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /public/examples/single_device.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { "t": "2015-02-02 15:59:52 +0000", 4 | "v": { 5 | "13": { 6 | "5": 2.0, 7 | "5_raw": 1450, 8 | "6": 13.319, 9 | "6_raw": 91390 10 | } 11 | } 12 | }, 13 | { "t": "2015-02-01 15:59:52 +0000", 14 | "v": { 15 | "13": { 16 | "5": 1.0, 17 | "5_raw": 550, 18 | "6": 14.19, 19 | "6_raw": 34390 20 | } 21 | } 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /public/examples/sockets.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Smart Citizen Websockets 6 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fablabbcn/smartcitizen-api/cfd1394f9efd0bf49cba3b30b40b4e326d32bc7b/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | # Build and publish image to DOCKER HUB 2 | 3 | set -ex 4 | USERNAME=smartcitizen 5 | IMAGE=api 6 | version=`cat VERSION` 7 | 8 | echo "VERSION: $version" 9 | 10 | docker build -t $USERNAME/$IMAGE:latest . 11 | docker tag $USERNAME/$IMAGE:latest $USERNAME/$IMAGE:$version 12 | docker push $USERNAME/$IMAGE:latest 13 | docker push $USERNAME/$IMAGE:$version 14 | -------------------------------------------------------------------------------- /scripts/Dockerfile-kairos: -------------------------------------------------------------------------------- 1 | FROM openjdk:8-jdk-alpine 2 | 3 | RUN apk upgrade libssl1.0 --update-cache && \ 4 | apk add wget \ 5 | ca-certificates \ 6 | gettext \ 7 | bash 8 | RUN wget -O /tmp/kairosdb_dl.tar.gz \ 9 | https://github.com/kairosdb/kairosdb/releases/download/v1.2.2/kairosdb-1.2.2-1.tar.gz 10 | 11 | RUN mkdir -p /opt/ && \ 12 | cd /opt/ && \ 13 | tar -xvf /tmp/kairosdb_dl.tar.gz 14 | 15 | COPY conf/kairosdb.properties /tmp/kairosdb.properties 16 | COPY runkairos.sh /usr/bin/runkairos.sh 17 | RUN chmod +x /usr/bin/runkairos.sh 18 | 19 | EXPOSE 4242 8080 2003 2004 20 | ENTRYPOINT [ "/usr/bin/runkairos.sh"] 21 | 22 | CMD [ "run" ] 23 | -------------------------------------------------------------------------------- /scripts/cassandra/cassandra-settings.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Cassandra Recommended Settings 3 | DefaultDependencies=no 4 | After=sysinit.target local-fs.target 5 | Before=cassandra.service 6 | 7 | [Service] 8 | Type=oneshot 9 | ExecStart=/bin/sh -c 'echo never > /sys/kernel/mm/transparent_hugepage/defrag && \ 10 | echo mq-deadline > /sys/block/sda/queue/scheduler && \ 11 | echo 0 > /sys/class/block/sda/queue/rotational && \ 12 | echo 8 > /sys/class/block/sda/queue/read_ahead_kb && \ 13 | echo 0 > /proc/sys/vm/zone_reclaim_mode && \ 14 | echo 32 > /sys/block/sda/queue/nr_requests' 15 | 16 | [Install] 17 | WantedBy=basic.target 18 | -------------------------------------------------------------------------------- /scripts/cassandra/cassandra-sysctl.conf: -------------------------------------------------------------------------------- 1 | # Settings from https://docs.datastax.com/en/cassandra-oss/3.0/cassandra/install/installRecommendSettings.html 2 | net.core.rmem_max = 16777216 3 | net.core.wmem_max = 16777216 4 | net.core.rmem_default = 16777216 5 | net.core.wmem_default = 16777216 6 | net.core.optmem_max = 40960 7 | net.ipv4.tcp_rmem = 4096 87380 16777216 8 | net.ipv4.tcp_wmem = 4096 65536 16777216i 9 | vm.max_map_count = 1048575 10 | -------------------------------------------------------------------------------- /scripts/cassandra/cassandra.service: -------------------------------------------------------------------------------- 1 | # /usr/lib/systemd/system/cassandra.service 2 | 3 | [Unit] 4 | Description=Cassandra 5 | After=network.target 6 | StartLimitInterval=200 7 | StartLimitBurst=5 8 | 9 | [Service] 10 | Type=forking 11 | PIDFile=/var/lib/cassandra/cassandra.pid 12 | User=cassandra 13 | Group=cassandra 14 | Environment="CASSANDRA_INCLUDE=/opt/cassandra/cassandra.in.sh" 15 | PassEnvironment="CASSANDRA_INCLUDE" 16 | ExecStart=/opt/cassandra/bin/cassandra -p /var/lib/cassandra/cassandra.pid 17 | Restart=always 18 | RestartSec=10 19 | SuccessExitStatus=143 20 | LimitMEMLOCK=infinity 21 | LimitNOFILE=10000 22 | LimitNPROC=32768 23 | LimitAS=infinity 24 | 25 | [Install] 26 | WantedBy=multi-user.target 27 | -------------------------------------------------------------------------------- /scripts/cassandra/env.example: -------------------------------------------------------------------------------- 1 | STORAGE_DIR=/var/lib/cassandra 2 | LOG_DIR=/var/log/cassandra 3 | CLUSTER_NAME=clustername # Cluster name 4 | SEEDS=127.0.0.1 # CSV list of nodes on current cluster 5 | LISTEN_ADDRESS=127.0.0.1 # Private IP of this node 6 | RPC_ADDRESS=127.0.0.1 # Private IP of this node 7 | AUTHENTICATOR=PasswordAuthenticator 8 | ENDPOINT_SNITCH=GossipingPropertyFileSnitch 9 | MAX_HEAP_SIZE=2048M 10 | HEAP_NEWSIZE=512M 11 | -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Always pull from master? What if staging should deploy 'dev' branch? 3 | git pull origin master; 4 | docker compose pull auth; 5 | # Accept containers as params. Supports starting only 'app db' f.x. 6 | docker compose build && docker compose up -d $@ 7 | 8 | # Do we want to auto migrate? 9 | # For now, we only check if migration is needed 10 | docker compose exec app bin/rails db:migrate:status 11 | #docker compose exec app bin/rails db:migrate 12 | 13 | echo $(date) $(git rev-parse HEAD) >> deploy_history.txt 14 | -------------------------------------------------------------------------------- /scripts/dev-tools/get-token.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | if [ $# -eq 0 ]; then 4 | echo "Username or password missing" 5 | echo "Usage: get_token.sh USER PASSWORD localhost:3000" 6 | exit 7 | fi 8 | 9 | curl -XPOST 'http://'$3'/v0/sessions?username='$1'&password='$2 -d '' 10 | -------------------------------------------------------------------------------- /scripts/dev-tools/query-readings.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | # You need to install curl and jq 4 | 5 | # Gets reading for device 4 6 | 7 | if [ "$1" = "localhost" ]; then 8 | echo "Querying localhost" 9 | curl -s http://localhost:3000/v0/devices/4 | jq '.data.sensors[2].value' 10 | else 11 | echo "Querying staging" 12 | curl -s http://staging-api.smartcitizen.me/v0/devices/4 | jq '.data.sensors[2].value' 13 | fi 14 | -------------------------------------------------------------------------------- /scripts/docker_backup_db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if ! [[ $1 ]]; then 3 | echo "Database name missing for BACKUP." 4 | echo "Usage: 'docker_backup_db.sh my_db_name'" 5 | exit 6 | fi 7 | 8 | #docker exec -i $(docker compose ps -q db) pg_dump -Upostgres $1 > dump_`date +%Y-%m-%d"_"%H_%M_%S`.sql 9 | docker exec -i smartcitizen-api-db-1 pg_dump -Upostgres $1 > backup/dump_`date +%Y-%m-%d"_"%H_%M_%S`.sql 10 | -------------------------------------------------------------------------------- /scripts/docker_restore_db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if ! [[ $1 ]]; then 3 | echo "Database name missing for RESTORE." 4 | echo "Usage: 'docker_restore_db.sh my_db_name'" 5 | exit 6 | fi 7 | 8 | docker exec -i $(docker compose ps -q db) psql -Upostgres $1 < dump_latest.sql 9 | -------------------------------------------------------------------------------- /scripts/grafana/agent.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | log_level: info 3 | metrics: 4 | wal_directory: /tmp/grafana-agent-wal 5 | global: 6 | scrape_interval: 15s 7 | integrations: 8 | node_exporter: 9 | enabled: true 10 | instance: ${PROMETHEUS_INSTANCE_LABEL} 11 | prometheus_remote_write: 12 | - url: ${PROMETHEUS_URL} 13 | basic_auth: 14 | username: ${PROMETHEUS_USERNAME} 15 | password: ${PROMETHEUS_PASSWORD} 16 | -------------------------------------------------------------------------------- /spec/controllers/v0/devices_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe V0::DevicesController do 4 | skip { is_expected.to permit(:name,:description,:mac_address,:latitude,:longitude,:elevation,:exposure,:meta,:user_tags).for(:create) } 5 | end 6 | -------------------------------------------------------------------------------- /spec/controllers/v0/discourse_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe V0::DiscourseController, type: :controller do 4 | 5 | end 6 | -------------------------------------------------------------------------------- /spec/controllers/v0/me_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe V0::MeController do 4 | skip { is_expected.to permit(:email,:username,:password,:city,:country_code,:url).for(:update) } 5 | end 6 | -------------------------------------------------------------------------------- /spec/controllers/v0/measurements_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe V0::MeasurementsController do 4 | it { is_expected.to permit(:name,:description,:unit).for(:create) } 5 | end 6 | -------------------------------------------------------------------------------- /spec/controllers/v0/oauth_applications_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe V0::OauthApplicationsController do 4 | skip { is_expected.to permit(:name,:description,:unit).for(:create) } 5 | end 6 | -------------------------------------------------------------------------------- /spec/controllers/v0/onboarding/device_registrations_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe V0::Onboarding::DeviceRegistrationsController, type: :controller do 4 | it { is_expected.to permit(:email).for(:find_user, verb: :post) } 5 | end 6 | -------------------------------------------------------------------------------- /spec/controllers/v0/onboarding/orphan_devices_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe V0::Onboarding::OrphanDevicesController, type: :controller do 4 | it { is_expected.to permit(:name, :description, :exposure, :latitude, :longitude, 5 | :user_tags).for(:create) } 6 | 7 | describe "save_orphan_device" do 8 | before do 9 | @controller = V0::Onboarding::OrphanDevicesController.new 10 | @controller.params = ActionController::Parameters.new 11 | 12 | allow_any_instance_of(OrphanDevice).to receive(:generate_token!).and_raise(ActiveRecord::RecordInvalid.new(OrphanDevice.new)) 13 | end 14 | 15 | it 'tries 10 times generating_token & saving it' do 16 | expect(@controller).to receive(:raise).with(Smartcitizen::UnprocessableEntity.new) 17 | expect_any_instance_of(OrphanDevice).to receive(:generate_token!).exactly(10).times 18 | @controller.send(:create) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/controllers/v0/readings_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe V0::ReadingsController do 4 | it "should use strong parameters" 5 | end 6 | -------------------------------------------------------------------------------- /spec/controllers/v0/sensors_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe V0::SensorsController do 4 | it { is_expected.to permit(:name,:description,:unit,:measurement_id).for(:create) } 5 | end 6 | -------------------------------------------------------------------------------- /spec/controllers/v0/tags_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe V0::TagsController do 4 | it { is_expected.to permit(:name,:description).for(:create) } 5 | end 6 | -------------------------------------------------------------------------------- /spec/controllers/v0/users_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe V0::UsersController do 4 | it { is_expected.to permit(:email,:username,:password,:city,:country_code,:url).for(:create) } 5 | end 6 | -------------------------------------------------------------------------------- /spec/controllers/v1/devices_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe V1::DevicesController do 4 | skip { is_expected.to permit(:name,:description,:mac_address,:latitude,:longitude,:elevation,:exposure,:meta,:user_tags).for(:create) } 5 | end 6 | -------------------------------------------------------------------------------- /spec/factories/backup_readings.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :backup_reading do 3 | data { "MyText" } 4 | mac { "MyString" } 5 | version { "MyString" } 6 | ip { "MyString" } 7 | created_at { "2015-11-18 14:32:27" } 8 | end 9 | 10 | end 11 | -------------------------------------------------------------------------------- /spec/factories/bad_readings.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :bad_reading do 3 | tags { 1 } 4 | source_ip { "MyString" } 5 | data { "" } 6 | created_at { "2015-10-07 17:22:01" } 7 | end 8 | 9 | end 10 | -------------------------------------------------------------------------------- /spec/factories/components.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :component do 3 | uuid { SecureRandom.uuid } 4 | association :device 5 | association :sensor 6 | end 7 | 8 | end 9 | -------------------------------------------------------------------------------- /spec/factories/devices.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :device do 3 | uuid { SecureRandom.uuid } 4 | association :owner, factory: :user 5 | sequence("name") { |n| "device#{n}"} 6 | description { "my device" } 7 | mac_address { Faker::Internet.mac_address } 8 | latitude { 41.3966908 } 9 | longitude { 2.1921909 } 10 | elevation { 100 } 11 | hardware_info { { "id":47,"uuid":"7d45fead-defd-4482-bc6a-a1b711879e2d" } } 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/factories/devices_inventory.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :device_inventory do 3 | report { "{'random_property':'random_result'}" } 4 | created_at { "2015-10-07 17:22:01" } 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/factories/devices_tags.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :devices_tag do 3 | association :device 4 | association :tag 5 | end 6 | 7 | end 8 | -------------------------------------------------------------------------------- /spec/factories/doorkeeper.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :access_grant, class: Doorkeeper::AccessGrant do 3 | sequence(:resource_owner_id) { |n| n } 4 | application 5 | redirect_uri { 'https://app.com/callback' } 6 | expires_in { 100 } 7 | scopes { 'public write' } 8 | end 9 | 10 | factory :access_token, class: Doorkeeper::AccessToken do 11 | sequence(:resource_owner_id) { |n| n } 12 | application 13 | expires_in { 2.hours } 14 | 15 | factory :clientless_access_token do 16 | application { nil } 17 | end 18 | end 19 | 20 | factory :application, class: Doorkeeper::Application do 21 | sequence(:name) { |n| "Application #{n}" } 22 | redirect_uri { 'https://app.com/callback' } 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/factories/experiment.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :experiment do 3 | sequence("name") { |n| "experiment#{n}"} 4 | description { "my experiment" } 5 | association :owner, factory: :user 6 | is_test { false } 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/factories/measurements.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :measurement do 3 | sequence(:name) { |i| "Temperature #{i}" } 4 | description { "How hot something is" } 5 | unit { "C" } 6 | end 7 | end -------------------------------------------------------------------------------- /spec/factories/orphan_devices.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :orphan_device do 3 | name { "OrphanDeviceName" } 4 | description { "OrphanDeviceDescription" } 5 | exposure { "indoor" } 6 | # same coordinates used for testing Device 7 | latitude { 41.3966908 } 8 | longitude { 2.1921909 } 9 | user_tags { "tag1,tag2" } 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/factories/sensors.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :sensor do 3 | name { "MiCS-2710" } 4 | description { "Metaloxide gas sensor" } 5 | unit { "KΩ" } 6 | default_key { "key_#{SecureRandom.alphanumeric(4)}"} 7 | end 8 | end -------------------------------------------------------------------------------- /spec/factories/tag_sensors.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :tag_sensor do 3 | name { "TagSensor1" } 4 | description { "TagSensorDescription1" } 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/factories/tags.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :tag do 3 | sequence(:name) { |n| "tag#{n}"} 4 | description { "tag description" } 5 | end 6 | 7 | end 8 | -------------------------------------------------------------------------------- /spec/factories/users.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :user do 3 | uuid { SecureRandom.uuid } 4 | sequence(:username) { |n| "user#{n}" } 5 | sequence(:email) { |n| "user#{n}@bitsushi.com" } 6 | password { "password1" } 7 | password_confirmation { "password1"} 8 | ts_and_cs { true } 9 | url { "http://www.yahoo.com" } 10 | role_mask { 0 } 11 | 12 | factory :admin do 13 | role_mask { 5 } 14 | end 15 | 16 | factory :researcher do 17 | role_mask { 2 } 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/fixtures/fake_device_data.csv: -------------------------------------------------------------------------------- 1 | TIME,TEMP,HUM 2 | ISO 8601,C,% 3 | Time,Temperature,Humidity 4 | ,55,56 5 | 2025-01-01T00:00:00Z,1.1,10.01 6 | 2025-01-01T00:01:00Z,2.2,20.02 7 | 2025-01-01T00:02:00Z,3.3,30.03 8 | 2025-01-01T00:03:00Z,4.4,40.04 9 | 2025-01-01T00:04:00Z,5.5,50.05 10 | 2025-01-01T00:05:00Z,null,50.05 11 | 2025-01-01T00:06:00Z,null,null 12 | -------------------------------------------------------------------------------- /spec/jobs/check_battery_level_below_job_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe CheckBatteryLevelBelowJob, type: :job do 4 | 5 | it 'should update notify_low_battery_timestamp and send email' do 6 | device = create(:device, notify_low_battery: true, updated_at: "2023-01-01 00:00:00") 7 | updated_at_before = device.updated_at 8 | time_before = device.notify_low_battery_timestamp 9 | device.update_columns(data: { "10": '11'}) 10 | 11 | expect(device.data["10"].to_i).to eq(11) 12 | 13 | CheckBatteryLevelBelowJob.perform_now 14 | 15 | device.reload 16 | expect(time_before).not_to eq(device.notify_low_battery_timestamp) 17 | expect(device.updated_at).to eq(updated_at_before) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/jobs/check_device_stopped_publishing_job_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe CheckDeviceStoppedPublishingJob, type: :job do 4 | pending "add some examples to (or delete) #{__FILE__}" 5 | end 6 | -------------------------------------------------------------------------------- /spec/jobs/checkup_notify_job_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe CheckupNotifyJob, type: :job do 4 | pending "add some examples to (or delete) #{__FILE__}" 5 | end 6 | -------------------------------------------------------------------------------- /spec/jobs/checkup_user_email_blank_job_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe CheckupUserEmailBlankJob, type: :job do 4 | pending "add some examples to (or delete) #{__FILE__}" 5 | end 6 | -------------------------------------------------------------------------------- /spec/jobs/delete_archived_users_job_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe DeleteArchivedUsersJob, type: :job do 4 | describe "#perform_later" do 5 | ActiveJob::Base.queue_adapter = :test 6 | 7 | it "should have an enqueued job" do 8 | expect { 9 | DeleteArchivedUsersJob.perform_later 10 | }.to have_enqueued_job 11 | end 12 | 13 | it "should delete all archived users, created_at at least 72 hours ago" do 14 | userNormal = create(:user, username: "normalUser") 15 | userArchived = create(:user, username: "dontDeleteMe", workflow_state: "archived", created_at: 71.hours.ago) 16 | userArchived = create(:user, username: "deleteMe1", workflow_state: "archived", created_at: 73.hours.ago) 17 | userArchived = create(:user, username: "deleteMe2", workflow_state: "archived", created_at: 74.hours.ago) 18 | expect { 19 | DeleteArchivedUsersJob.perform_now 20 | }.to change(User.unscoped, :count).by(-2) 21 | end 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /spec/jobs/delete_orphaned_devices_job_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe DeleteOrphanedDevicesJob, type: :job do 4 | 5 | describe "#perform_later" do 6 | ActiveJob::Base.queue_adapter = :test 7 | 8 | it "should have an enqueued job" do 9 | expect { 10 | DeleteArchivedUsersJob.perform_later 11 | }.to have_enqueued_job 12 | end 13 | 14 | it "should delete all orphaned devices, older than 24 hours" do 15 | orp = create(:orphan_device, name: "dontDeleteMe", device_token: '123460', updated_at: 1.days.ago) 16 | orp = create(:orphan_device, name: "dontDeleteMe", device_token: '123457', updated_at: 8.days.ago) 17 | orp = create(:orphan_device, name: "dontDeleteMe", device_token: '123458', updated_at: 9.days.ago) 18 | 19 | expect(OrphanDevice.count).to eq 3 20 | 21 | expect { 22 | DeleteOrphanedDevicesJob.perform_now 23 | }.to change(OrphanDevice, :count).by(-2) 24 | 25 | expect(OrphanDevice.count).to eq 1 26 | end 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /spec/jobs/send_to_datastore_job_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe SendToDatastoreJob, type: :job do 4 | pending "add some examples to (or delete) #{__FILE__}" 5 | end 6 | -------------------------------------------------------------------------------- /spec/lib/mqtt_forwarder_spec.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fablabbcn/smartcitizen-api/cfd1394f9efd0bf49cba3b30b40b4e326d32bc7b/spec/lib/mqtt_forwarder_spec.rb -------------------------------------------------------------------------------- /spec/mailers/previews/user_mailer_preview.rb: -------------------------------------------------------------------------------- 1 | class UserMailerPreview < ActionMailer::Preview 2 | 3 | def welcome_email 4 | UserMailer.with(user: User.first).welcome(User.first.id) 5 | end 6 | 7 | def password_reset 8 | UserMailer.password_reset(User.first.id) 9 | end 10 | 11 | def device_archive 12 | UserMailer.device_archive(User.first.devices.first.id, User.first.id) 13 | end 14 | 15 | def device_battery_low 16 | UserMailer.device_battery_low(User.last.devices.first.id) 17 | end 18 | 19 | def device_stopped_publishing 20 | UserMailer.device_stopped_publishing(User.last.devices.first.id) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/models/devices_tag_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe DevicesTag, type: :model do 4 | pending "add some examples to (or delete) #{__FILE__}" 5 | end 6 | -------------------------------------------------------------------------------- /spec/models/measurement_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Measurement, type: :model do 4 | it { is_expected.to have_many(:sensors) } 5 | 6 | it { is_expected.to validate_uniqueness_of(:name) } 7 | it { is_expected.to validate_presence_of(:name) } 8 | it { is_expected.to validate_presence_of(:description) } 9 | end 10 | -------------------------------------------------------------------------------- /spec/models/sensor_tag_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe TagSensor, type: :model do 4 | 5 | let(:the_sensor) { build(:tag_sensor) } 6 | 7 | context 'SensorTag' do 8 | it "has a name and description from the factory" do 9 | expect( the_sensor.name ).to eq('TagSensor1') 10 | expect( the_sensor.description ).to eq('TagSensorDescription1') 11 | end 12 | 13 | it { is_expected.to validate_presence_of(:name) } 14 | it { should have_many(:sensors).through(:sensor_tags) } 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/models/tag_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Tag, type: :model do 4 | pending "add some examples to (or delete) #{__FILE__}" 5 | end 6 | -------------------------------------------------------------------------------- /spec/policies/application_policy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe ApplicationPolicy do 4 | subject { ApplicationPolicy.new(user, Device.new) } 5 | 6 | context "for a visitor" do 7 | let(:user) { nil } 8 | it { is_expected.to_not permitz(:index) } 9 | it { is_expected.to_not permitz(:new) } 10 | it { is_expected.to_not permitz(:show) } 11 | it { is_expected.to_not permitz(:edit) } 12 | it { is_expected.to_not permitz(:create) } 13 | it { is_expected.to_not permitz(:update) } 14 | it { is_expected.to_not permitz(:destroy) } 15 | end 16 | 17 | end -------------------------------------------------------------------------------- /spec/policies/component_policy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe ComponentPolicy do 4 | subject { ComponentPolicy.new(user, component) } 5 | let(:component) { FactoryBot.build(:component) } 6 | 7 | context "for a visitor" do 8 | let(:user) { nil } 9 | it { is_expected.to permitz(:show) } 10 | end 11 | 12 | context "for a user" do 13 | let(:user) { FactoryBot.build(:user) } 14 | it { is_expected.to permitz(:show) } 15 | end 16 | 17 | context "for an admin" do 18 | let(:user) { FactoryBot.build(:admin) } 19 | it { is_expected.to permitz(:show) } 20 | end 21 | 22 | end 23 | -------------------------------------------------------------------------------- /spec/policies/measurement_policy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe MeasurementPolicy do 4 | subject { MeasurementPolicy.new(user, measurement) } 5 | 6 | let(:measurement) { FactoryBot.build(:measurement) } 7 | 8 | context "for a visitor" do 9 | let(:user) { nil } 10 | it { is_expected.to permitz(:show) } 11 | it { is_expected.to_not permitz(:update) } 12 | it { is_expected.to_not permitz(:create) } 13 | end 14 | 15 | context "for a user" do 16 | let(:user) { FactoryBot.create(:user) } 17 | it { is_expected.to permitz(:show) } 18 | it { is_expected.to_not permitz(:update) } 19 | it { is_expected.to_not permitz(:create) } 20 | end 21 | 22 | context "for an admin" do 23 | let(:user) { FactoryBot.create(:admin) } 24 | it { is_expected.to permitz(:show) } 25 | it { is_expected.to permitz(:update) } 26 | it { is_expected.to permitz(:create) } 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /spec/policies/oauth_application_policy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe OauthApplicationPolicy do 4 | 5 | it "needs specs" 6 | 7 | end -------------------------------------------------------------------------------- /spec/policies/password_reset_policy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe PasswordResetPolicy do 4 | subject { PasswordResetPolicy.new(user, password_reset) } 5 | 6 | let(:password_reset) { FactoryBot.create(:user, password_reset_token: '12345') } 7 | 8 | context "for a visitor" do 9 | let(:user) { nil } 10 | it { is_expected.to permitz(:show) } 11 | it { is_expected.to_not permitz(:create) } 12 | it { is_expected.to_not permitz(:update) } 13 | end 14 | 15 | context "for a general user" do 16 | let(:user) { FactoryBot.create(:user) } 17 | it { is_expected.to permitz(:show) } 18 | it { is_expected.to permitz(:create) } 19 | it { is_expected.to_not permitz(:update) } 20 | end 21 | 22 | context "for the requesting user" do 23 | let(:user) { password_reset } 24 | it { is_expected.to permitz(:show) } 25 | it { is_expected.to permitz(:create) } 26 | it { is_expected.to permitz(:update) } 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /spec/policies/reading_policy_spec.rb: -------------------------------------------------------------------------------- 1 | # require 'spec_helper' 2 | 3 | # describe ReadingPolicy do 4 | # subject { ReadingPolicy.new(user, reading) } 5 | 6 | # let(:reading) { FactoryBot.create(:reading) } 7 | 8 | # skip "for a visitor" do 9 | # let(:user) { nil } 10 | # it { is_expected.to permitz(:show) } 11 | # it { is_expected.to permitz(:create) } 12 | # end 13 | 14 | # end -------------------------------------------------------------------------------- /spec/policies/sensor_policy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe SensorPolicy do 4 | subject { SensorPolicy.new(user, sensor) } 5 | 6 | let(:sensor) { FactoryBot.build(:sensor) } 7 | 8 | context "for a visitor" do 9 | let(:user) { nil } 10 | it { is_expected.to permitz(:show) } 11 | it { is_expected.to_not permitz(:update) } 12 | it { is_expected.to_not permitz(:create) } 13 | it { is_expected.to_not permitz(:destroy) } 14 | end 15 | 16 | context "for a user" do 17 | let(:user) { FactoryBot.create(:user) } 18 | it { is_expected.to permitz(:show) } 19 | it { is_expected.to_not permitz(:update) } 20 | it { is_expected.to_not permitz(:create) } 21 | it { is_expected.to_not permitz(:destroy) } 22 | end 23 | 24 | context "for an admin" do 25 | let(:user) { FactoryBot.create(:admin) } 26 | it { is_expected.to permitz(:show) } 27 | it { is_expected.to permitz(:update) } 28 | it { is_expected.to permitz(:create) } 29 | it { is_expected.to permitz(:destroy) } 30 | end 31 | 32 | end 33 | -------------------------------------------------------------------------------- /spec/policies/tag_policy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe TagPolicy do 4 | subject { TagPolicy.new(user, tag) } 5 | 6 | let(:tag) { FactoryBot.create(:tag) } 7 | 8 | context "for a visitor" do 9 | let(:user) { nil } 10 | it { is_expected.to permitz(:show) } 11 | it { is_expected.to_not permitz(:update) } 12 | it { is_expected.to_not permitz(:create) } 13 | it { is_expected.to_not permitz(:destroy) } 14 | end 15 | 16 | context "for a user" do 17 | let(:user) { FactoryBot.create(:user) } 18 | it { is_expected.to permitz(:show) } 19 | it { is_expected.to_not permitz(:update) } 20 | it { is_expected.to_not permitz(:create) } 21 | it { is_expected.to_not permitz(:destroy) } 22 | end 23 | 24 | context "for an admin" do 25 | let(:user) { FactoryBot.create(:admin) } 26 | it { is_expected.to permitz(:show) } 27 | it { is_expected.to permitz(:update) } 28 | it { is_expected.to permitz(:create) } 29 | it { is_expected.to permitz(:destroy) } 30 | end 31 | 32 | end 33 | -------------------------------------------------------------------------------- /spec/policies/user_policy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe UserPolicy do 4 | subject { UserPolicy.new(user, usermodel) } 5 | 6 | let(:usermodel) { FactoryBot.create(:user) } 7 | 8 | context "for a visitor" do 9 | let(:user) { nil } 10 | it { is_expected.to permitz(:show) } 11 | it { is_expected.to permitz(:create) } 12 | it { is_expected.to_not permitz(:update) } 13 | it { is_expected.to_not permitz(:destroy) } 14 | it { is_expected.to permitz(:request_password_reset) } 15 | it { is_expected.to permitz(:update_password) } 16 | end 17 | 18 | context "for a user" do 19 | let(:user) { usermodel } 20 | it { is_expected.to permitz(:show) } 21 | it { is_expected.to_not permitz(:create) } 22 | it { is_expected.to permitz(:update) } 23 | it { is_expected.to permitz(:destroy) } 24 | it { is_expected.to_not permitz(:request_password_reset) } 25 | it { is_expected.to_not permitz(:update_password) } 26 | end 27 | 28 | end 29 | -------------------------------------------------------------------------------- /spec/requests/v0/application_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe V0::ApplicationController do 4 | 5 | describe "format" do 6 | it "(JSON) returns ugly JSON, with JSON Mimetype" do 7 | json = api_get '/devices' 8 | #expect( response.body.to_s ).to_not eq( JSON.pretty_generate(json) ) 9 | expect(response.header['Content-Type']).to include('application/json') 10 | end 11 | 12 | skip "(JSON) returns pretty JSON, with JSON Mimetype if ?pretty=true" do 13 | json = api_get '/v0/devices?pretty=true' 14 | expect( response.body.to_s ).to eq( JSON.pretty_generate(json) ) 15 | expect(response.header['Content-Type']).to include('application/json') 16 | end 17 | 18 | skip "(JSON-P) returns JS Mimetype if callback param present" do 19 | # rails now handles this 20 | api_get '/v0/devices?callback=something' 21 | expect(response.header['Content-Type']).to include('text/javascript') 22 | end 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /spec/requests/v0/errors_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe V0::ErrorsController do 4 | 5 | describe "GET 404" do 6 | it "returns 404 error" do 7 | j = api_get '/404' 8 | expect(j['id']).to eq('not_found') 9 | expect(response.status).to eq(404) 10 | expect(response.body).to match("Endpoint not found") 11 | end 12 | end 13 | 14 | skip "raises 500 error" do 15 | j = api_get '/test_error' 16 | expect(j['id']).to eq('internal_server_error') 17 | expect(response.status).to eq(500) 18 | end 19 | 20 | end 21 | -------------------------------------------------------------------------------- /spec/requests/v0/oauth_applications_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe V0::OauthApplicationsController do 4 | 5 | let(:application) { create :application } 6 | let(:user) { create :user } 7 | let(:token) { create :access_token, application: application, resource_owner_id: user.id } 8 | let(:admin) { create :admin } 9 | let(:admin_token) { create :access_token, application: application, resource_owner_id: admin.id } 10 | 11 | it "needs specs" 12 | 13 | end 14 | -------------------------------------------------------------------------------- /spec/support/api_macros.rb: -------------------------------------------------------------------------------- 1 | module ApiMacros 2 | 3 | def api_get action, p={}, version="0", h={} 4 | get "/v#{version}/#{action}", params:p, headers:h 5 | JSON.parse(response.body) rescue {} 6 | end 7 | 8 | def api_post action, p={}, version="0", h={} 9 | post "/v#{version}/#{action}", params:p, headers:h 10 | JSON.parse(response.body) rescue {} 11 | end 12 | 13 | def api_delete action, p={}, version="0", h={} 14 | delete "/v#{version}/#{action}", params:p, headers:h 15 | JSON.parse(response.body) rescue {} 16 | end 17 | 18 | def api_put action, p={}, version="0", h={} 19 | patch "/v#{version}/#{action}", params:p, headers:h 20 | JSON.parse(response.body) rescue {} 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /spec/support/env_vars.rb: -------------------------------------------------------------------------------- 1 | module EnvVars 2 | 3 | def set_env_var(name, value) 4 | allow(ENV).to receive(:[]).with(name).and_return(value) 5 | end 6 | 7 | end 8 | -------------------------------------------------------------------------------- /spec/support/mailer_macros.rb: -------------------------------------------------------------------------------- 1 | module MailerMacros 2 | def last_email 3 | ActionMailer::Base.deliveries.last 4 | end 5 | 6 | def reset_email 7 | ActionMailer::Base.deliveries = [] 8 | end 9 | end 10 | 11 | RSpec.configure do |config| 12 | 13 | config.before(:each) do 14 | reset_email 15 | end 16 | 17 | end 18 | -------------------------------------------------------------------------------- /spec/support/pundit_macros.rb: -------------------------------------------------------------------------------- 1 | # Why .permitz? The global namespace is a bit overcrowded. 2 | # If you can think of a better term then please change it. 3 | 4 | RSpec::Matchers.define :permitz do |action| 5 | match do |policy| 6 | policy.public_send("#{action}?") 7 | end 8 | 9 | failure_message do |policy| 10 | "#{policy.class} does not permit #{action} on #{policy.record} for #{policy.user.inspect}." 11 | end 12 | 13 | failure_message_when_negated do |policy| 14 | "#{policy.class} does not forbid #{action} on #{policy.record} for #{policy.user.inspect}." 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /tmp/grafana_wal_data/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fablabbcn/smartcitizen-api/cfd1394f9efd0bf49cba3b30b40b4e326d32bc7b/tmp/grafana_wal_data/.keep --------------------------------------------------------------------------------