├── .app_version
├── .circleci
└── config.yml
├── .devcontainer
├── Dockerfile
├── devcontainer.json
└── docker-compose.yml
├── .env.development
├── .env.template
├── .env.test
├── .gitattributes
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ └── bug_report.md
├── dependabot.yml
└── workflows
│ ├── build_and_push.yml
│ └── ci.yml
├── .gitignore
├── .rspec
├── .rubocop.yml
├── .ruby-version
├── CHANGELOG.md
├── CONTRIBUTING.md
├── DEVELOPMENT.md
├── Gemfile
├── Gemfile.lock
├── LICENSE
├── Procfile
├── Procfile.dev
├── Procfile.prometheus.dev
├── README.md
├── Rakefile
├── app.json
├── app
├── assets
│ ├── builds
│ │ ├── .keep
│ │ └── tailwind.css
│ ├── config
│ │ └── manifest.js
│ ├── images
│ │ ├── .keep
│ │ ├── favicon.jpeg
│ │ └── favicon
│ │ │ ├── android-chrome-192x192.png
│ │ │ ├── android-chrome-512x512.png
│ │ │ ├── apple-touch-icon.png
│ │ │ ├── browserconfig.xml.erb
│ │ │ ├── favicon-16x16.png
│ │ │ ├── favicon-32x32.png
│ │ │ ├── favicon.ico
│ │ │ ├── mstile-150x150.png
│ │ │ ├── safari-pinned-tab.svg
│ │ │ └── site.webmanifest.erb
│ └── stylesheets
│ │ ├── actiontext.css
│ │ ├── application.css
│ │ └── application.tailwind.css
├── channels
│ ├── application_cable
│ │ ├── channel.rb
│ │ └── connection.rb
│ ├── imports_channel.rb
│ ├── notifications_channel.rb
│ └── points_channel.rb
├── controllers
│ ├── api
│ │ └── v1
│ │ │ ├── areas_controller.rb
│ │ │ ├── countries
│ │ │ ├── borders_controller.rb
│ │ │ └── visited_cities_controller.rb
│ │ │ ├── health_controller.rb
│ │ │ ├── maps
│ │ │ └── tile_usage_controller.rb
│ │ │ ├── overland
│ │ │ └── batches_controller.rb
│ │ │ ├── owntracks
│ │ │ └── points_controller.rb
│ │ │ ├── photos_controller.rb
│ │ │ ├── points
│ │ │ └── tracked_months_controller.rb
│ │ │ ├── points_controller.rb
│ │ │ ├── settings_controller.rb
│ │ │ ├── stats_controller.rb
│ │ │ ├── subscriptions_controller.rb
│ │ │ ├── users_controller.rb
│ │ │ ├── visits
│ │ │ └── possible_places_controller.rb
│ │ │ └── visits_controller.rb
│ ├── api_controller.rb
│ ├── application_controller.rb
│ ├── concerns
│ │ └── .keep
│ ├── exports_controller.rb
│ ├── home_controller.rb
│ ├── imports_controller.rb
│ ├── map_controller.rb
│ ├── notifications_controller.rb
│ ├── places_controller.rb
│ ├── points_controller.rb
│ ├── settings
│ │ ├── background_jobs_controller.rb
│ │ ├── maps_controller.rb
│ │ └── users_controller.rb
│ ├── settings_controller.rb
│ ├── stats_controller.rb
│ ├── trips_controller.rb
│ └── visits_controller.rb
├── helpers
│ ├── application_helper.rb
│ ├── country_flag_helper.rb
│ ├── points_helper.rb
│ └── trips_helper.rb
├── javascript
│ ├── application.js
│ ├── channels
│ │ ├── consumer.js
│ │ ├── imports_channel.js
│ │ ├── index.js
│ │ ├── notifications_channel.js
│ │ └── points_channel.js
│ ├── controllers
│ │ ├── application.js
│ │ ├── base_controller.js
│ │ ├── checkbox_select_all_controller.js
│ │ ├── datetime_controller.js
│ │ ├── direct_upload_controller.js
│ │ ├── imports_controller.js
│ │ ├── index.js
│ │ ├── map_preview_controller.js
│ │ ├── maps_controller.js
│ │ ├── notifications_controller.js
│ │ ├── removals_controller.js
│ │ ├── trip_map_controller.js
│ │ ├── trips_controller.js
│ │ ├── visit_modal_map_controller.js
│ │ ├── visit_modal_places_controller.js
│ │ ├── visit_name_controller.js
│ │ └── visits_map_controller.js
│ ├── maps
│ │ ├── areas.js
│ │ ├── country_codes.js
│ │ ├── fog_of_war.js
│ │ ├── helpers.js
│ │ ├── layers.js
│ │ ├── markers.js
│ │ ├── polylines.js
│ │ ├── popups.js
│ │ ├── raster_maps_config.js
│ │ ├── tile_monitor.js
│ │ ├── vector_maps_config.js
│ │ └── visits.js
│ └── styles
│ │ └── visits.css
├── jobs
│ ├── app_version_checking_job.rb
│ ├── application_job.rb
│ ├── area_visits_calculating_job.rb
│ ├── area_visits_calculation_scheduling_job.rb
│ ├── bulk_stats_calculating_job.rb
│ ├── bulk_visits_suggesting_job.rb
│ ├── cache
│ │ ├── cleaning_job.rb
│ │ └── preheating_job.rb
│ ├── data_migrations
│ │ ├── migrate_places_lonlat_job.rb
│ │ ├── migrate_points_latlon_job.rb
│ │ ├── set_points_country_ids_job.rb
│ │ ├── set_reverse_geocoded_at_for_points_job.rb
│ │ └── start_settings_points_country_ids_job.rb
│ ├── enqueue_background_job.rb
│ ├── export_job.rb
│ ├── import
│ │ ├── google_takeout_job.rb
│ │ ├── immich_geodata_job.rb
│ │ ├── photoprism_geodata_job.rb
│ │ ├── process_job.rb
│ │ ├── update_points_count_job.rb
│ │ └── watcher_job.rb
│ ├── jobs
│ │ └── clean_finished_job.rb
│ ├── overland
│ │ └── batch_creating_job.rb
│ ├── owntracks
│ │ └── point_creating_job.rb
│ ├── points
│ │ └── create_job.rb
│ ├── reverse_geocoding_job.rb
│ ├── stats
│ │ └── calculating_job.rb
│ ├── trips
│ │ ├── calculate_all_job.rb
│ │ ├── calculate_countries_job.rb
│ │ ├── calculate_distance_job.rb
│ │ └── calculate_path_job.rb
│ └── visit_suggesting_job.rb
├── mailers
│ └── application_mailer.rb
├── models
│ ├── application_record.rb
│ ├── area.rb
│ ├── concerns
│ │ ├── .keep
│ │ ├── distanceable.rb
│ │ ├── nearable.rb
│ │ └── point_validation.rb
│ ├── country.rb
│ ├── export.rb
│ ├── import.rb
│ ├── notification.rb
│ ├── place.rb
│ ├── place_visit.rb
│ ├── point.rb
│ ├── stat.rb
│ ├── trip.rb
│ ├── user.rb
│ ├── visit.rb
│ └── visit_draft.rb
├── policies
│ └── application_policy.rb
├── serializers
│ ├── api
│ │ ├── photo_serializer.rb
│ │ ├── place_serializer.rb
│ │ ├── point_serializer.rb
│ │ ├── slim_point_serializer.rb
│ │ └── visit_serializer.rb
│ ├── export_serializer.rb
│ ├── point_serializer.rb
│ ├── points
│ │ ├── geojson_serializer.rb
│ │ └── gpx_serializer.rb
│ └── stats_serializer.rb
├── services
│ ├── areas
│ │ └── visits
│ │ │ └── create.rb
│ ├── cache
│ │ └── clean.rb
│ ├── check_app_version.rb
│ ├── countries_and_cities.rb
│ ├── exception_reporter.rb
│ ├── exports
│ │ └── create.rb
│ ├── geojson
│ │ ├── importer.rb
│ │ └── params.rb
│ ├── google_maps
│ │ ├── phone_takeout_importer.rb
│ │ ├── records_importer.rb
│ │ ├── records_storage_importer.rb
│ │ └── semantic_history_importer.rb
│ ├── gpx
│ │ └── track_importer.rb
│ ├── immich
│ │ ├── import_geodata.rb
│ │ └── request_photos.rb
│ ├── imports
│ │ ├── broadcaster.rb
│ │ ├── create.rb
│ │ ├── destroy.rb
│ │ ├── secure_file_downloader.rb
│ │ └── watcher.rb
│ ├── jobs
│ │ └── create.rb
│ ├── metrics
│ │ └── maps
│ │ │ └── tile_usage
│ │ │ └── track.rb
│ ├── notifications
│ │ └── create.rb
│ ├── overland
│ │ └── params.rb
│ ├── own_tracks
│ │ ├── importer.rb
│ │ ├── params.rb
│ │ └── rec_parser.rb
│ ├── photoprism
│ │ ├── cache_preview_token.rb
│ │ ├── import_geodata.rb
│ │ └── request_photos.rb
│ ├── photos
│ │ ├── importer.rb
│ │ ├── search.rb
│ │ └── thumbnail.rb
│ ├── points
│ │ ├── create.rb
│ │ ├── params.rb
│ │ └── raw_data_lonlat_extractor.rb
│ ├── points_limit_exceeded.rb
│ ├── reverse_geocoding
│ │ ├── places
│ │ │ └── fetch_data.rb
│ │ └── points
│ │ │ └── fetch_data.rb
│ ├── stats
│ │ ├── bulk_calculator.rb
│ │ └── calculate_month.rb
│ ├── subscription
│ │ └── decode_jwt_token.rb
│ ├── tasks
│ │ └── imports
│ │ │ └── google_records.rb
│ ├── tracks
│ │ └── build_path.rb
│ ├── trips
│ │ └── photos.rb
│ ├── users
│ │ └── safe_settings.rb
│ └── visits
│ │ ├── bulk_update.rb
│ │ ├── creator.rb
│ │ ├── detector.rb
│ │ ├── find_in_time.rb
│ │ ├── find_within_bounding_box.rb
│ │ ├── finder.rb
│ │ ├── group.rb
│ │ ├── merge_service.rb
│ │ ├── merger.rb
│ │ ├── names
│ │ ├── builder.rb
│ │ ├── fetcher.rb
│ │ └── suggester.rb
│ │ ├── place_finder.rb
│ │ ├── smart_detect.rb
│ │ ├── suggest.rb
│ │ └── time_chunks.rb
└── views
│ ├── active_storage
│ └── blobs
│ │ └── _blob.html.erb
│ ├── application
│ └── _favicon.html.erb
│ ├── devise
│ ├── confirmations
│ │ └── new.html.erb
│ ├── mailer
│ │ ├── confirmation_instructions.html.erb
│ │ ├── email_changed.html.erb
│ │ ├── password_change.html.erb
│ │ ├── reset_password_instructions.html.erb
│ │ └── unlock_instructions.html.erb
│ ├── passwords
│ │ ├── edit.html.erb
│ │ └── new.html.erb
│ ├── registrations
│ │ ├── _api_key.html.erb
│ │ ├── _points_usage.html.erb
│ │ ├── edit.html.erb
│ │ └── new.html.erb
│ ├── sessions
│ │ └── new.html.erb
│ ├── shared
│ │ ├── _error_messages.html.erb
│ │ └── _links.html.erb
│ └── unlocks
│ │ └── new.html.erb
│ ├── exports
│ └── index.html.erb
│ ├── home
│ └── index.html.erb
│ ├── imports
│ ├── _form.html.erb
│ ├── _import.html.erb
│ ├── edit.html.erb
│ ├── index.html.erb
│ ├── new.html.erb
│ └── show.html.erb
│ ├── layouts
│ ├── action_text
│ │ └── contents
│ │ │ └── _content.html.erb
│ ├── application.html.erb
│ ├── mailer.html.erb
│ └── mailer.text.erb
│ ├── map
│ ├── _settings_modals.html.erb
│ └── index.html.erb
│ ├── notifications
│ ├── _notification.html.erb
│ ├── index.html.erb
│ └── show.html.erb
│ ├── places
│ └── index.html.erb
│ ├── points
│ ├── _point.html.erb
│ └── index.html.erb
│ ├── settings
│ ├── _navigation.html.erb
│ ├── background_jobs
│ │ └── index.html.erb
│ ├── index.html.erb
│ ├── maps
│ │ └── index.html.erb
│ ├── subscriptions
│ │ └── index.html.erb
│ └── users
│ │ ├── edit.html.erb
│ │ └── index.html.erb
│ ├── shared
│ ├── _flash.html.erb
│ ├── _footer.html.erb
│ ├── _legal_footer.html.erb
│ └── _navbar.html.erb
│ ├── stats
│ ├── _reverse_geocoding_stats.html.erb
│ ├── _stat.html.erb
│ ├── _year.html.erb
│ ├── index.html.erb
│ └── show.html.erb
│ ├── trips
│ ├── _countries.html.erb
│ ├── _distance.html.erb
│ ├── _form.html.erb
│ ├── _path.html.erb
│ ├── _trip.html.erb
│ ├── edit.html.erb
│ ├── index.html.erb
│ ├── new.html.erb
│ └── show.html.erb
│ └── visits
│ ├── _buttons.html.erb
│ ├── _modal.html.erb
│ ├── _name.html.erb
│ ├── _visit.html.erb
│ └── index.html.erb
├── bin
├── dev
├── importmap
├── jobs
├── rails
├── rake
├── rubocop
└── setup
├── config.ru
├── config
├── application.rb
├── boot.rb
├── cable.yml
├── cache.yml
├── database.ci.yml
├── database.yml
├── environment.rb
├── environments
│ ├── development.rb
│ ├── production.rb
│ └── test.rb
├── favicon.json
├── importmap.rb
├── initializers
│ ├── 00_random.rb
│ ├── 01_constants.rb
│ ├── 03_dawarich_settings.rb
│ ├── assets.rb
│ ├── aws.rb
│ ├── cache_jobs.rb
│ ├── content_security_policy.rb
│ ├── devise.rb
│ ├── filter_parameter_logging.rb
│ ├── geocoder.rb
│ ├── httparty.rb
│ ├── inflections.rb
│ ├── mime_types.rb
│ ├── new_framework_defaults_8_0.rb
│ ├── permissions_policy.rb
│ ├── prometheus.rb
│ ├── rswag_api.rb
│ ├── rswag_ui.rb
│ ├── sentry.rb
│ ├── sidekiq.rb
│ ├── strong_migrations.rb
│ └── web_app_manifest.rb
├── locales
│ ├── devise.en.yml
│ └── en.yml
├── puma.rb
├── queue.yml
├── recurring.yml
├── routes.rb
├── schedule.yml
├── sidekiq.yml
├── storage.yml
└── tailwind.config.js
├── db
├── cable_schema.rb
├── cache_schema.rb
├── data
│ ├── 20240525110530_bind_existing_points_to_first_user.rb
│ ├── 20240610170930_remove_points_without_coordinates.rb
│ ├── 20240625201842_add_fog_of_war_meters_to_settings.rb
│ ├── 20240713103122_make_first_user_admin.rb
│ ├── 20240724141417_add_visit_settings_to_user.rb
│ ├── 20240730130922_add_route_opacity_to_settings.rb
│ ├── 20240808133112_run_initial_visit_suggestion.rb
│ ├── 20240815174852_add_owntracks_points_data.rb
│ ├── 20240822094532_add_counter_cache_to_imports.rb
│ ├── 20241022100309_add_points_rendering_mode_to_settings.rb
│ ├── 20241107112451_add_live_map_enabled_to_settings.rb
│ ├── 20241202125248_set_reverse_geocoded_at_for_points.rb
│ ├── 20241206163450_create_telemetry_notification.rb
│ ├── 20250104204852_create_photon_load_notification.rb
│ ├── 20250120154554_remove_duplicate_points.rb
│ ├── 20250123151849_create_paths_for_trips.rb
│ ├── 20250222213848_migrate_points_latlon.rb
│ ├── 20250226192005_activate_selfhosted_users.rb
│ ├── 20250303194123_migrate_places_lonlat.rb
│ ├── 20250403204658_update_imports_points_count.rb
│ ├── 20250404182629_set_active_until_for_selfhosted_users.rb
│ ├── 20250516180933_set_points_country_ids.rb
│ ├── 20250518173936_fix_france_codes.rb
│ └── 20250518174305_set_default_distance_unit_for_user.rb
├── data_schema.rb
├── migrate
│ ├── 20220325100310_devise_create_users.rb
│ ├── 20231021104256_add_service_name_to_active_storage_blobs.active_storage.rb
│ ├── 20231021104257_create_active_storage_variant_records.active_storage.rb
│ ├── 20231021104258_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb
│ ├── 20240315213523_create_points.rb
│ ├── 20240315215423_create_imports.rb
│ ├── 20240317171559_add_indicies_to_points_latitude_longitude.rb
│ ├── 20240323125126_add_raw_points_and_doubles_to_import.rb
│ ├── 20240323160300_create_stats.rb
│ ├── 20240323161049_add_index_to_points_timestamp.rb
│ ├── 20240323190039_add_user_id_to_stat.rb
│ ├── 20240324161309_create_active_storage_tables.active_storage.rb
│ ├── 20240324161800_add_processed_to_imports.rb
│ ├── 20240324173315_add_daily_distance_to_stat.rb
│ ├── 20240404154959_add_api_key_to_users.rb
│ ├── 20240425200155_add_raw_data_to_imports.rb
│ ├── 20240518095848_add_theme_to_users.rb
│ ├── 20240525110244_add_user_id_to_points.rb
│ ├── 20240612152451_create_exports.rb
│ ├── 20240620205120_add_settings_to_users.rb
│ ├── 20240630093005_add_fog_of_war_to_default_settings.rb
│ ├── 20240703105734_create_notifications.rb
│ ├── 20240712141303_add_geodata_to_points.rb
│ ├── 20240713103051_add_admin_to_users.rb
│ ├── 20240721165313_create_areas.rb
│ ├── 20240721183005_create_visits.rb
│ ├── 20240721183116_add_visit_id_to_points.rb
│ ├── 20240805150111_create_places.rb
│ ├── 20240808102348_add_place_id_to_visits.rb
│ ├── 20240808102425_make_area_id_optional_in_visits.rb
│ ├── 20240808121027_create_place_visits.rb
│ ├── 20240822092405_add_points_count_to_imports.rb
│ ├── 20241127161621_create_trips.rb
│ ├── 20241128095325_create_action_text_tables.action_text.rb
│ ├── 20241202114820_add_reverse_geocoded_at_to_points.rb
│ ├── 20241205160055_add_devise_trackable_columns_to_users.rb
│ ├── 20241211113119_add_started_at_index_to_visits.rb
│ ├── 20241226202204_add_database_users_constraints.rb
│ ├── 20241226202831_validate_add_database_users_constraints.rb
│ ├── 20250120152014_add_course_and_course_accuracy_to_points.rb
│ ├── 20250120152540_add_external_track_id_to_points.rb
│ ├── 20250120154555_add_unique_index_to_points.rb
│ ├── 20250123145155_enable_postgis_extension.rb
│ ├── 20250123151657_add_path_to_trips.rb
│ ├── 20250219195822_add_status_to_users.rb
│ ├── 20250221181805_add_lonlat_to_points.rb
│ ├── 20250221185032_add_lonlat_index.rb
│ ├── 20250221194430_remove_points_latitude_longitude_uniqueness_index.rb
│ ├── 20250221194509_add_unique_lon_lat_index_to_points.rb
│ ├── 20250303194009_add_lonlat_to_places.rb
│ ├── 20250303194043_add_lonlat_index_to_places.rb
│ ├── 20250324180755_add_format_start_at_end_at_to_exports.rb
│ ├── 20250404182437_add_active_until_to_users.rb
│ ├── 20250513164521_add_visited_countries_to_trips.rb
│ ├── 20250515190752_create_countries.rb
│ └── 20250515192211_add_country_id_to_points.rb
├── queue_schema.rb
├── schema.rb
└── seeds.rb
├── docker
├── .dockerignore
├── Dockerfile.dev
├── Dockerfile.prod
├── docker-compose.production.yml
├── docker-compose.yml
├── sidekiq-entrypoint.sh
└── web-entrypoint.sh
├── docs
├── How_to_extract_geodata_from_photos.md
├── How_to_install_Dawarich_in_k8s.md
├── How_to_install_Dawarich_on_Synology.md
├── How_to_install_Dawarich_using_Docker.md
├── how_to_setup_reverse_proxy.md
└── synology
│ ├── .env
│ ├── docker-compose.yml
│ ├── spk.tgz
│ └── update.sh
├── lib
├── assets
│ ├── .keep
│ └── countries.geojson
├── tasks
│ ├── .keep
│ ├── data_cleanup.rake
│ ├── exports.rake
│ ├── import.rake
│ ├── imports.rake
│ ├── points.rake
│ ├── rswag.rake
│ └── users.rake
└── timestamps.rb
├── log
└── .keep
├── package-lock.json
├── package.json
├── postgresql.conf.example
├── public
├── .well-known
│ └── apple-app-site-association
├── 400.html
├── 404.html
├── 406-unsupported-browser.html
├── 422.html
├── 500.html
├── apple-touch-icon-precomposed.png
├── apple-touch-icon.png
├── exports
│ └── .keep
├── favicon.ico
├── icon.png
├── icon.svg
└── robots.txt
├── screenshots
├── imports.jpeg
├── map.jpeg
└── stats.jpeg
├── spec
├── channels
│ ├── imports_channel_spec.rb
│ ├── notifications_channel_spec.rb
│ └── points_channel_spec.rb
├── factories
│ ├── areas.rb
│ ├── countries.rb
│ ├── exports.rb
│ ├── imports.rb
│ ├── notifications.rb
│ ├── place_visits.rb
│ ├── places.rb
│ ├── points.rb
│ ├── stats.rb
│ ├── trips.rb
│ ├── users.rb
│ └── visits.rb
├── fixtures
│ └── files
│ │ ├── geojson
│ │ ├── export.json
│ │ ├── export_same_points.json
│ │ └── gpslogger_example.json
│ │ ├── google
│ │ ├── location-history.json
│ │ ├── location-history
│ │ │ ├── with_activitySegment_with_startLocation.json
│ │ │ ├── with_activitySegment_with_startLocation_timestampMs.json
│ │ │ ├── with_activitySegment_with_startLocation_timestamp_in_milliseconds_format.json
│ │ │ ├── with_activitySegment_with_startLocation_timestamp_in_seconds_format.json
│ │ │ ├── with_activitySegment_with_startLocation_with_iso_timestamp.json
│ │ │ ├── with_activitySegment_without_startLocation.json
│ │ │ ├── with_activitySegment_without_startLocation_without_waypointPath.json
│ │ │ ├── with_placeVisit_with_location_with_coordinates.json
│ │ │ ├── with_placeVisit_with_location_with_coordinates_with_iso_timestamp.json
│ │ │ ├── with_placeVisit_with_location_with_coordinates_with_milliseconds_timestamp.json
│ │ │ ├── with_placeVisit_with_location_with_coordinates_with_seconds_timestamp.json
│ │ │ ├── with_placeVisit_with_location_with_coordinates_with_timestampMs.json
│ │ │ ├── with_placeVisit_without_location_with_coordinates.json
│ │ │ └── with_placeVisit_without_location_with_coordinates_with_otherCandidateLocations.json
│ │ ├── phone-takeout.json
│ │ ├── records.json
│ │ └── semantic_history.json
│ │ ├── gpx
│ │ ├── arc_example.gpx
│ │ ├── garmin_example.gpx
│ │ ├── gpx_track_multiple_segments.gpx
│ │ ├── gpx_track_multiple_tracks.gpx
│ │ └── gpx_track_single_segment.gpx
│ │ ├── immich
│ │ ├── geodata.json
│ │ └── response.json
│ │ ├── overland
│ │ └── geodata.json
│ │ ├── owntracks
│ │ └── 2024-03.rec
│ │ ├── points
│ │ └── geojson_example.json
│ │ └── watched
│ │ ├── invalid_user@domain.com
│ │ └── location-history.json
│ │ └── user@domain.com
│ │ ├── 2023_January.json
│ │ ├── Records.json
│ │ ├── export_same_points.json
│ │ ├── gpx_track_single_segment.gpx
│ │ ├── location-history.json
│ │ └── owntracks.rec
├── jobs
│ ├── app_version_checking_job_spec.rb
│ ├── area_visits_calculating_job_spec.rb
│ ├── area_visits_calculation_scheduling_job_spec.rb
│ ├── bulk_stats_calculating_job_spec.rb
│ ├── bulk_visits_suggesting_job_spec.rb
│ ├── data_migrations
│ │ ├── migrate_places_lonlat_job_spec.rb
│ │ ├── migrate_points_latlon_job_spec.rb
│ │ ├── set_points_country_ids_job_spec.rb
│ │ └── start_settings_points_country_ids_job_spec.rb
│ ├── enqueue_background_job_spec.rb
│ ├── export_job_spec.rb
│ ├── import
│ │ ├── immich_geodata_job_spec.rb
│ │ ├── process_job_spec.rb
│ │ └── watcher_job_spec.rb
│ ├── overland
│ │ └── batch_creating_job_spec.rb
│ ├── owntracks
│ │ └── point_creating_job_spec.rb
│ ├── points
│ │ └── create_job_spec.rb
│ ├── reverse_geocoding_job_spec.rb
│ ├── stats
│ │ └── calculating_job_spec.rb
│ └── visit_suggesting_job_spec.rb
├── lib
│ └── dawarich_settings_spec.rb
├── models
│ ├── area_spec.rb
│ ├── concerns
│ │ └── point_validation_spec.rb
│ ├── country_spec.rb
│ ├── export_spec.rb
│ ├── import_spec.rb
│ ├── notification_spec.rb
│ ├── place_spec.rb
│ ├── place_visit_spec.rb
│ ├── point_spec.rb
│ ├── stat_spec.rb
│ ├── trip_spec.rb
│ ├── user_spec.rb
│ └── visit_spec.rb
├── rails_helper.rb
├── requests
│ ├── api
│ │ └── v1
│ │ │ ├── areas_spec.rb
│ │ │ ├── countries
│ │ │ ├── borders_spec.rb
│ │ │ └── visited_cities_spec.rb
│ │ │ ├── health_spec.rb
│ │ │ ├── maps
│ │ │ └── tile_usage_spec.rb
│ │ │ ├── overland
│ │ │ └── batches_spec.rb
│ │ │ ├── owntracks
│ │ │ └── points_spec.rb
│ │ │ ├── photos_spec.rb
│ │ │ ├── points
│ │ │ └── tracked_months_spec.rb
│ │ │ ├── points_spec.rb
│ │ │ ├── settings_spec.rb
│ │ │ ├── stats_spec.rb
│ │ │ ├── subscriptions_spec.rb
│ │ │ ├── users_spec.rb
│ │ │ ├── visits
│ │ │ └── possible_places_spec.rb
│ │ │ └── visits_spec.rb
│ ├── authentication_spec.rb
│ ├── exports_spec.rb
│ ├── home_spec.rb
│ ├── imports_spec.rb
│ ├── map_spec.rb
│ ├── notifications_spec.rb
│ ├── places_spec.rb
│ ├── points_spec.rb
│ ├── settings
│ │ ├── background_jobs_spec.rb
│ │ ├── maps_spec.rb
│ │ └── users_spec.rb
│ ├── settings_spec.rb
│ ├── sidekiq_spec.rb
│ ├── stats_spec.rb
│ ├── trips_spec.rb
│ ├── users_spec.rb
│ └── visits_spec.rb
├── serializers
│ ├── api
│ │ ├── photo_serializer_spec.rb
│ │ ├── place_serializer_spec.rb
│ │ ├── point_serializer_spec.rb
│ │ ├── slim_point_serializer_spec.rb
│ │ └── visit_serializer_spec.rb
│ ├── export_serializer_spec.rb
│ ├── point_serializer_spec.rb
│ ├── points
│ │ ├── geojson_serializer_spec.rb
│ │ └── gpx_serializer_spec.rb
│ └── stats_serializer_spec.rb
├── services
│ ├── areas
│ │ └── visits
│ │ │ └── create_spec.rb
│ ├── check_app_version_spec.rb
│ ├── countries_and_cities_spec.rb
│ ├── exports
│ │ └── create_spec.rb
│ ├── geojson
│ │ ├── importer_spec.rb
│ │ └── params_spec.rb
│ ├── google_maps
│ │ ├── phone_takeout_importer_spec.rb
│ │ ├── records_importer_spec.rb
│ │ ├── records_storage_importer_spec.rb
│ │ └── semantic_history_importer_spec.rb
│ ├── gpx
│ │ └── track_importer_spec.rb
│ ├── immich
│ │ ├── import_geodata_spec.rb
│ │ └── request_photos_spec.rb
│ ├── imports
│ │ ├── create_spec.rb
│ │ ├── destroy_spec.rb
│ │ ├── secure_file_downloader_spec.rb
│ │ └── watcher_spec.rb
│ ├── jobs
│ │ └── create_spec.rb
│ ├── metrics
│ │ └── maps
│ │ │ └── tile_usage
│ │ │ └── track_spec.rb
│ ├── notifications
│ │ └── create_spec.rb
│ ├── overland
│ │ └── params_spec.rb
│ ├── own_tracks
│ │ ├── importer_spec.rb
│ │ └── params_spec.rb
│ ├── photoprism
│ │ ├── cache_preview_token_spec.rb
│ │ ├── import_geodata_spec.rb
│ │ └── request_photos_spec.rb
│ ├── photos
│ │ ├── importer_spec.rb
│ │ ├── search_spec.rb
│ │ └── thumbnail_spec.rb
│ ├── points
│ │ ├── create_spec.rb
│ │ ├── params_spec.rb
│ │ └── raw_data_lonlat_extractor_spec.rb
│ ├── points_limit_exceeded_spec.rb
│ ├── reverse_geocoding
│ │ ├── places
│ │ │ └── fetch_data_spec.rb
│ │ └── points
│ │ │ └── fetch_data_spec.rb
│ ├── stats
│ │ └── calculate_month_spec.rb
│ ├── tasks
│ │ └── imports
│ │ │ └── google_records_spec.rb
│ ├── tracks
│ │ └── build_path_spec.rb
│ ├── trips
│ │ └── photos_spec.rb
│ ├── users
│ │ └── safe_settings_spec.rb
│ └── visits
│ │ ├── bulk_update_spec.rb
│ │ ├── creator_spec.rb
│ │ ├── detector_spec.rb
│ │ ├── find_in_time_spec.rb
│ │ ├── find_within_bounding_box_spec.rb
│ │ ├── finder_spec.rb
│ │ ├── group_spec.rb
│ │ ├── merge_service_spec.rb
│ │ ├── merger_spec.rb
│ │ ├── names
│ │ ├── builder_spec.rb
│ │ └── suggester_spec.rb
│ │ ├── place_finder_spec.rb
│ │ ├── smart_detect_spec.rb
│ │ ├── suggest_spec.rb
│ │ └── time_chunks_spec.rb
├── spec_helper.rb
├── support
│ ├── capybara.rb
│ ├── devise.rb
│ ├── geocoder_stubs.rb
│ ├── map_layer_helpers.rb
│ ├── polyline_popup_helpers.rb
│ ├── pundit_matchers.rb
│ ├── shared_examples
│ │ └── map_examples.rb
│ └── system_helpers.rb
├── swagger
│ └── api
│ │ └── v1
│ │ ├── areas_controller_spec.rb
│ │ ├── countries
│ │ └── visited_cities_spec.rb
│ │ ├── health_controller_spec.rb
│ │ ├── overland
│ │ └── batches_controller_spec.rb
│ │ ├── owntracks
│ │ └── points_controller_spec.rb
│ │ ├── photos_controller_spec.rb
│ │ ├── points
│ │ └── tracked_months_controller_spec.rb
│ │ ├── points_controller_spec.rb
│ │ ├── settings_controller_spec.rb
│ │ ├── stats_controller_spec.rb
│ │ └── users_controller_spec.rb
├── swagger_helper.rb
├── system
│ ├── README.md
│ ├── authentication_spec.rb
│ └── map_interaction_spec.rb
└── tasks
│ └── import_spec.rb
├── storage
└── .keep
├── swagger
└── v1
│ └── swagger.yaml
├── test
├── application_system_test_case.rb
├── channels
│ └── application_cable
│ │ └── connection_test.rb
├── controllers
│ └── .keep
├── fixtures
│ └── files
│ │ └── .keep
├── helpers
│ └── .keep
├── integration
│ └── .keep
├── mailers
│ └── .keep
├── models
│ └── .keep
├── system
│ └── .keep
└── test_helper.rb
├── tests
└── system
│ └── test_scenarios.md
├── tmp
├── .keep
├── imports
│ └── watched
│ │ ├── .keep
│ │ └── put-your-directory-here.txt
├── pids
│ └── .keep
└── storage
│ └── .keep
└── vendor
├── .keep
└── javascript
├── .keep
├── leaflet-draw.js
├── leaflet-providers.js
├── leaflet.heat.js
└── leaflet.js
/.app_version:
--------------------------------------------------------------------------------
1 | 0.27.1
2 |
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Ruby and Node DevContainer",
3 | "dockerComposeFile": ["docker-compose.yml"],
4 | "service": "dawarich_dev",
5 | "settings": {
6 | "terminal.integrated.defaultProfile.linux": "bash"
7 | },
8 | "extensions": [
9 | "rebornix.ruby", // Ruby-Support
10 | "esbenp.prettier-vscode", // Prettier for JS-Formating
11 | "dbaeumer.vscode-eslint" // ESLint for JavaScript
12 | ],
13 | "postCreateCommand": "yarn install && bundle config set --local path 'vendor/bundle' && bundle install --jobs 20 --retry 5",
14 | "forwardPorts": [3000], // Redirect to Rails-App-Server
15 | "remoteUser": "root",
16 | "workspaceFolder": "/var/app"
17 | }
18 |
--------------------------------------------------------------------------------
/.env.development:
--------------------------------------------------------------------------------
1 | DATABASE_HOST=localhost
2 | DATABASE_USERNAME=postgres
3 | DATABASE_PASSWORD=password
4 | DATABASE_NAME=dawarich_development
5 | DATABASE_PORT=5432
6 | REDIS_URL=redis://localhost:6379/1
7 |
--------------------------------------------------------------------------------
/.env.template:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/.env.template
--------------------------------------------------------------------------------
/.env.test:
--------------------------------------------------------------------------------
1 | DATABASE_HOST=localhost
2 | DATABASE_USERNAME=postgres
3 | DATABASE_PASSWORD=password
4 | DATABASE_NAME=dawarich_test
5 | DATABASE_PORT=5432
6 | REDIS_URL=redis://localhost:6379/1
7 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # See https://git-scm.com/docs/gitattributes for more about git attribute files.
2 |
3 | # Mark the database schema as having been generated.
4 | db/schema.rb linguist-generated
5 |
6 | # Mark any vendored files as having been vendored.
7 | vendor/* linguist-vendored
8 |
--------------------------------------------------------------------------------
/.github/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: freika # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: freika
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 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12 | polar: # Replace with a single Polar username
13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
15 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **OS & Hardware**
11 | Provide your software and hardware specs
12 |
13 | **Version**
14 | Provide the version of Dawarich you're experiencing the problem on.
15 |
16 | **Describe the bug**
17 | A clear and concise description of what the bug is.
18 |
19 | **To Reproduce**
20 | Steps to reproduce the behavior:
21 | 1. Go to '...'
22 | 2. Click on '....'
23 | 3. Scroll down to '....'
24 | 4. See error
25 |
26 | **Expected behavior**
27 | A clear and concise description of what you expected to happen.
28 |
29 | **Screenshots**
30 | If applicable, add screenshots to help explain your problem.
31 |
32 | **Logs**
33 | If applicable, add logs from containers `dawarich_app` and `dawarich_sidekiq` to help explain your problem.
34 |
35 | **Additional context**
36 | Add any other context about the problem here.
37 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: 'bundler'
9 | directory: '/'
10 | schedule:
11 | interval: 'weekly'
12 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --require spec_helper
2 | --profile
3 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | require: rubocop-rails
2 |
3 | Style/Documentation:
4 | Enabled: false
5 |
6 | Style/ClassAndModuleChildren:
7 | Enabled: false
8 |
9 | Layout/HashAlignment:
10 | Enabled: false
11 |
12 | Metrics/BlockLength:
13 | Enabled: false
14 |
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | 3.4.1
2 |
--------------------------------------------------------------------------------
/DEVELOPMENT.md:
--------------------------------------------------------------------------------
1 | If you want to develop with dawarich you can use the devcontainer, with your IDE. It is tested with visual studio code.
2 |
3 | Load the directory in Vs-Code and press F1. And Run the command: `Dev Containers: Rebuild Containers` after a while you should see a terminal.
4 |
5 | Now you can create/prepare the Database (this need to be done once):
6 | ```bash
7 | bundle exec rails db:prepare
8 | ```
9 |
10 | Afterwards you can run sidekiq:
11 | ```bash
12 | bundle exec sidekiq
13 |
14 | ```
15 |
16 | And in a second terminal the dawarich-app:
17 | ```bash
18 | bundle exec bin/dev
19 | ```
20 |
21 | You can connect with a web browser to http://127.0.0.l:3000/ and login with the default credentials.
22 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: bundle exec puma -C config/puma.rb
2 | worker: bundle exec bin/jobs
3 |
--------------------------------------------------------------------------------
/Procfile.dev:
--------------------------------------------------------------------------------
1 | web: bin/rails server -p 3000 -b ::
2 |
--------------------------------------------------------------------------------
/Procfile.prometheus.dev:
--------------------------------------------------------------------------------
1 | prometheus_exporter: bundle exec prometheus_exporter -b ANY
2 | web: bin/rails server -p 3000 -b ::
3 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Add your own tasks in files placed in lib/tasks ending in .rake,
4 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
5 |
6 | require_relative 'config/application'
7 |
8 | Rails.application.load_tasks
9 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dawarich",
3 | "description": "Dawarich",
4 | "buildpacks": [
5 | { "url": "https://github.com/heroku/heroku-buildpack-nodejs.git" },
6 | { "url": "https://github.com/heroku/heroku-buildpack-ruby.git" }
7 | ],
8 | "scripts": {
9 | "dokku": {
10 | "predeploy": "bundle exec rails db:migrate"
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/app/assets/builds/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/app/assets/builds/.keep
--------------------------------------------------------------------------------
/app/assets/config/manifest.js:
--------------------------------------------------------------------------------
1 | //= link_tree ../images
2 | //= link_directory ../stylesheets .css
3 | //= link_tree ../builds
4 | //= link_tree ../../javascript .js
5 | //= link_tree ../../../vendor/javascript .js
6 | //= link favicon/browserconfig.xml
7 |
--------------------------------------------------------------------------------
/app/assets/images/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/app/assets/images/.keep
--------------------------------------------------------------------------------
/app/assets/images/favicon.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/app/assets/images/favicon.jpeg
--------------------------------------------------------------------------------
/app/assets/images/favicon/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/app/assets/images/favicon/android-chrome-192x192.png
--------------------------------------------------------------------------------
/app/assets/images/favicon/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/app/assets/images/favicon/android-chrome-512x512.png
--------------------------------------------------------------------------------
/app/assets/images/favicon/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/app/assets/images/favicon/apple-touch-icon.png
--------------------------------------------------------------------------------
/app/assets/images/favicon/browserconfig.xml.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #da532c
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/assets/images/favicon/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/app/assets/images/favicon/favicon-16x16.png
--------------------------------------------------------------------------------
/app/assets/images/favicon/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/app/assets/images/favicon/favicon-32x32.png
--------------------------------------------------------------------------------
/app/assets/images/favicon/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/app/assets/images/favicon/favicon.ico
--------------------------------------------------------------------------------
/app/assets/images/favicon/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/app/assets/images/favicon/mstile-150x150.png
--------------------------------------------------------------------------------
/app/assets/images/favicon/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
8 | Created by potrace 1.14, written by Peter Selinger 2001-2017
9 |
10 |
12 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/assets/images/favicon/site.webmanifest.erb:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Dawarich",
3 | "short_name": "Dawarich",
4 | "icons": [
5 | {
6 | "src": "<%= asset_path 'favicon/android-chrome-192x192.png' %>",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "<%= asset_path 'favicon/android-chrome-512x512.png' %>",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#ffffff",
17 | "background_color": "#ffffff",
18 | "display": "standalone"
19 | }
20 |
--------------------------------------------------------------------------------
/app/channels/application_cable/channel.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Channel < ActionCable::Channel::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/app/channels/application_cable/connection.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ApplicationCable
4 | class Connection < ActionCable::Connection::Base
5 | identified_by :current_user
6 |
7 | def connect
8 | self.current_user = find_verified_user
9 | end
10 |
11 | private
12 |
13 | def find_verified_user
14 | if (verified_user = env['warden'].user)
15 | verified_user
16 | else
17 | reject_unauthorized_connection
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/app/channels/imports_channel.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ImportsChannel < ApplicationCable::Channel
4 | def subscribed
5 | stream_for current_user
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/app/channels/notifications_channel.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class NotificationsChannel < ApplicationCable::Channel
4 | def subscribed
5 | stream_for current_user
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/app/channels/points_channel.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class PointsChannel < ApplicationCable::Channel
4 | def subscribed
5 | stream_for current_user
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/app/controllers/api/v1/countries/borders_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Api::V1::Countries::BordersController < ApplicationController
4 | def index
5 | countries = Rails.cache.fetch('dawarich/countries_codes', expires_in: 1.day) do
6 | Oj.load(File.read(Rails.root.join('lib/assets/countries.geojson')))
7 | end
8 |
9 | render json: countries
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/controllers/api/v1/countries/visited_cities_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Api::V1::Countries::VisitedCitiesController < ApiController
4 | before_action :validate_params
5 |
6 | def index
7 | start_at = DateTime.parse(params[:start_at]).to_i
8 | end_at = DateTime.parse(params[:end_at]).to_i
9 |
10 | points = current_api_user
11 | .tracked_points
12 | .where(timestamp: start_at..end_at)
13 |
14 | render json: { data: CountriesAndCities.new(points).call }
15 | end
16 |
17 | private
18 |
19 | def required_params
20 | %i[start_at end_at]
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/app/controllers/api/v1/health_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Api::V1::HealthController < ApiController
4 | skip_before_action :authenticate_api_key
5 |
6 | def index
7 | if current_api_user
8 | response.set_header('X-Dawarich-Response', 'Hey, I\'m alive and authenticated!')
9 | else
10 | response.set_header('X-Dawarich-Response', 'Hey, I\'m alive!')
11 | end
12 |
13 | response.set_header('X-Dawarich-Version', APP_VERSION)
14 |
15 | render json: { status: 'ok' }
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/app/controllers/api/v1/maps/tile_usage_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Api::V1::Maps::TileUsageController < ApiController
4 | def create
5 | Metrics::Maps::TileUsage::Track.new(current_api_user.id, tile_usage_params[:count].to_i).call
6 |
7 | head :ok
8 | end
9 |
10 | private
11 |
12 | def tile_usage_params
13 | params.require(:tile_usage).permit(:count)
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/app/controllers/api/v1/overland/batches_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Api::V1::Overland::BatchesController < ApiController
4 | before_action :authenticate_active_api_user!, only: %i[create]
5 | before_action :validate_points_limit, only: %i[create]
6 |
7 | def create
8 | Overland::BatchCreatingJob.perform_later(batch_params, current_api_user.id)
9 |
10 | render json: { result: 'ok' }, status: :created
11 | end
12 |
13 | private
14 |
15 | def batch_params
16 | params.permit(locations: [:type, { geometry: {}, properties: {} }], batch: {})
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/app/controllers/api/v1/owntracks/points_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Api::V1::Owntracks::PointsController < ApiController
4 | before_action :authenticate_active_api_user!, only: %i[create]
5 | before_action :validate_points_limit, only: %i[create]
6 |
7 | def create
8 | Owntracks::PointCreatingJob.perform_later(point_params, current_api_user.id)
9 |
10 | render json: {}, status: :ok
11 | end
12 |
13 | private
14 |
15 | def point_params
16 | params.permit!
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/app/controllers/api/v1/points/tracked_months_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Api::V1::Points::TrackedMonthsController < ApiController
4 | def index
5 | render json: current_api_user.years_tracked
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/app/controllers/api/v1/stats_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Api::V1::StatsController < ApiController
4 | def index
5 | render json: StatsSerializer.new(current_api_user).call
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/app/controllers/api/v1/subscriptions_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Api::V1::SubscriptionsController < ApiController
4 | skip_before_action :authenticate_api_key, only: %i[callback]
5 |
6 | def callback
7 | decoded_token = Subscription::DecodeJwtToken.new(params[:token]).call
8 |
9 | user = User.find(decoded_token[:user_id])
10 | user.update!(status: decoded_token[:status], active_until: decoded_token[:active_until])
11 |
12 | render json: { message: 'Subscription updated successfully' }
13 | rescue JWT::DecodeError => e
14 | ExceptionReporter.call(e)
15 | render json: { message: 'Failed to verify subscription update.' }, status: :unauthorized
16 | rescue ArgumentError => e
17 | ExceptionReporter.call(e)
18 | render json: { message: 'Invalid subscription data received.' }, status: :unprocessable_entity
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/app/controllers/api/v1/users_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Api::V1::UsersController < ApiController
4 | def me
5 | render json: { user: current_api_user }
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/app/controllers/api/v1/visits/possible_places_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Api::V1::Visits::PossiblePlacesController < ApiController
4 | def index
5 | visit = current_api_user.visits.find(params[:id])
6 | possible_places = visit.suggested_places.map do |place|
7 | Api::PlaceSerializer.new(place).call
8 | end
9 |
10 | render json: possible_places
11 | rescue ActiveRecord::RecordNotFound
12 | render json: { error: 'Visit not found' }, status: :not_found
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/app/controllers/concerns/.keep
--------------------------------------------------------------------------------
/app/controllers/home_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class HomeController < ApplicationController
4 | def index
5 | # redirect_to 'https://dawarich.app', allow_other_host: true and return unless SELF_HOSTED
6 |
7 | redirect_to map_url if current_user
8 |
9 | @points = current_user.tracked_points.without_raw_data if current_user
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/controllers/places_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class PlacesController < ApplicationController
4 | before_action :authenticate_user!
5 | before_action :set_place, only: :destroy
6 |
7 | def index
8 | @places = current_user.places.page(params[:page]).per(20)
9 | end
10 |
11 | def destroy
12 | @place.destroy!
13 |
14 | redirect_to places_url, notice: 'Place was successfully destroyed.', status: :see_other
15 | end
16 |
17 | private
18 |
19 | def set_place
20 | @place = current_user.places.find(params[:id])
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/app/controllers/settings/maps_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Settings::MapsController < ApplicationController
4 | before_action :authenticate_user!
5 |
6 | def index
7 | @maps = current_user.safe_settings.maps
8 |
9 | @tile_usage = 7.days.ago.to_date.upto(Time.zone.today).map do |date|
10 | [
11 | date.to_s,
12 | Rails.cache.read("dawarich_map_tiles_usage:#{current_user.id}:#{date}") || 0
13 | ]
14 | end
15 | end
16 |
17 | def update
18 | current_user.settings['maps'] = settings_params
19 | current_user.save!
20 |
21 | redirect_to settings_maps_path, notice: 'Settings updated'
22 | end
23 |
24 | private
25 |
26 | def settings_params
27 | params.require(:maps).permit(:name, :url, :distance_unit)
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/app/helpers/country_flag_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module CountryFlagHelper
4 | def country_flag(country_name)
5 | country_code = country_to_code(country_name)
6 | return "" unless country_code
7 |
8 | # Convert country code to regional indicator symbols (flag emoji)
9 | country_code.upcase.each_char.map { |c| (c.ord + 127397).chr(Encoding::UTF_8) }.join
10 | end
11 |
12 |
13 | private
14 |
15 | def country_to_code(country_name)
16 | mapping = Country.names_to_iso_a2
17 |
18 | return mapping[country_name] if mapping[country_name]
19 |
20 | mapping.each do |name, code|
21 | return code if country_name.downcase == name.downcase
22 | return code if country_name.downcase.include?(name.downcase) || name.downcase.include?(country_name.downcase)
23 | end
24 |
25 | nil
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/app/helpers/points_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module PointsHelper
4 | def link_to_date(timestamp)
5 | datetime = Time.zone.at(timestamp)
6 |
7 | link_to map_path(start_at: datetime.beginning_of_day, end_at: datetime.end_of_day), \
8 | class: 'underline hover:no-underline' do
9 | datetime.strftime('%d.%m.%Y')
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/app/javascript/application.js:
--------------------------------------------------------------------------------
1 | // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
2 |
3 | import "@rails/actioncable"
4 | import "controllers"
5 | import "@hotwired/turbo-rails"
6 |
7 | import "leaflet"
8 | import "leaflet-providers"
9 | import "chartkick"
10 | import "Chart.bundle"
11 | import "./channels"
12 |
13 | import "trix"
14 | import "@rails/actiontext"
15 |
--------------------------------------------------------------------------------
/app/javascript/channels/consumer.js:
--------------------------------------------------------------------------------
1 | // Action Cable provides the framework to deal with WebSockets in Rails.
2 | // You can generate new channels where WebSocket features live using the `bin/rails generate channel` command.
3 |
4 | import { createConsumer } from "@rails/actioncable"
5 |
6 | export default createConsumer()
7 |
--------------------------------------------------------------------------------
/app/javascript/channels/imports_channel.js:
--------------------------------------------------------------------------------
1 | import consumer from "./consumer"
2 |
3 | consumer.subscriptions.create("ImportsChannel", {
4 | connected() {
5 | // console.log("Connected to the imports channel!");
6 | },
7 |
8 | disconnected() {
9 | // Called when the subscription has been terminated by the server
10 | },
11 |
12 | received(data) {
13 | // Called when there's incoming data on the websocket for this channel
14 | }
15 | });
16 |
--------------------------------------------------------------------------------
/app/javascript/channels/index.js:
--------------------------------------------------------------------------------
1 | // Import all the channels to be used by Action Cable
2 | import "notifications_channel"
3 | import "points_channel"
4 | import "imports_channel"
5 |
--------------------------------------------------------------------------------
/app/javascript/channels/notifications_channel.js:
--------------------------------------------------------------------------------
1 | import consumer from "./consumer"
2 |
3 | consumer.subscriptions.create("NotificationsChannel", {
4 | connected() {
5 | // console.log("Connected to the notifications channel!");
6 | },
7 |
8 | disconnected() {
9 | // Called when the subscription has been terminated by the server
10 | },
11 |
12 | received(data) {
13 | // Called when there's incoming data on the websocket for this channel
14 | }
15 | });
16 |
--------------------------------------------------------------------------------
/app/javascript/channels/points_channel.js:
--------------------------------------------------------------------------------
1 | import consumer from "./consumer"
2 |
3 | consumer.subscriptions.create("PointsChannel", {
4 | connected() {
5 | // Called when the subscription is ready for use on the server
6 | },
7 |
8 | disconnected() {
9 | // Called when the subscription has been terminated by the server
10 | },
11 |
12 | received(data) {
13 | // Called when there's incoming data on the websocket for this channel
14 | }
15 | });
16 |
--------------------------------------------------------------------------------
/app/javascript/controllers/application.js:
--------------------------------------------------------------------------------
1 | import { Application } from "@hotwired/stimulus"
2 |
3 | const application = Application.start()
4 |
5 | // Configure Stimulus development experience
6 | application.debug = false
7 | window.Stimulus = application
8 |
9 | export { application }
10 |
--------------------------------------------------------------------------------
/app/javascript/controllers/base_controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "@hotwired/stimulus"
2 |
3 | export default class extends Controller {
4 | static values = {
5 | selfHosted: Boolean
6 | }
7 |
8 | // Every controller that extends BaseController and uses initialize()
9 | // should call super.initialize()
10 | // Example:
11 | // export default class extends BaseController {
12 | // initialize() {
13 | // super.initialize()
14 | // }
15 | // }
16 | initialize() {
17 | // Get the self-hosted value from the HTML root element
18 | if (!this.hasSelfHostedValue) {
19 | const selfHosted = document.documentElement.dataset.selfHosted === 'true'
20 | this.selfHostedValue = selfHosted
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/javascript/controllers/checkbox_select_all_controller.js:
--------------------------------------------------------------------------------
1 | import BaseController from "./base_controller"
2 |
3 | // Connects to data-controller="checkbox-select-all"
4 | export default class extends BaseController {
5 | static targets = ["parent", "child"]
6 |
7 | connect() {
8 | this.parentTarget.checked = false
9 | this.childTargets.map(x => x.checked = false)
10 | }
11 |
12 | toggleChildren() {
13 | if (this.parentTarget.checked) {
14 | this.childTargets.map(x => x.checked = true)
15 | } else {
16 | this.childTargets.map(x => x.checked = false)
17 | }
18 | }
19 |
20 | toggleParent() {
21 | if (this.childTargets.map(x => x.checked).includes(false)) {
22 | this.parentTarget.checked = false
23 | } else {
24 | this.parentTarget.checked = true
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/javascript/controllers/index.js:
--------------------------------------------------------------------------------
1 | import { application } from "controllers/application"
2 |
3 | // Eager load all controllers defined in the import map under controllers/**/*_controller
4 | import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
5 | eagerLoadControllersFrom("controllers", application)
6 |
--------------------------------------------------------------------------------
/app/javascript/controllers/removals_controller.js:
--------------------------------------------------------------------------------
1 | import BaseController from "./base_controller"
2 |
3 | export default class extends BaseController {
4 | static values = {
5 | timeout: Number
6 | }
7 |
8 | connect() {
9 | if (this.timeoutValue) {
10 | setTimeout(() => {
11 | this.remove()
12 | }, this.timeoutValue)
13 | }
14 | }
15 |
16 | remove() {
17 | this.element.classList.add('fade-out')
18 | setTimeout(() => {
19 | this.element.remove()
20 |
21 | // Remove the container if it's empty
22 | const container = document.getElementById('flash-messages')
23 | if (container && !container.hasChildNodes()) {
24 | container.remove()
25 | }
26 | }, 150)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/javascript/styles/visits.css:
--------------------------------------------------------------------------------
1 | .visit-checkbox-container {
2 | z-index: 10;
3 | opacity: 0;
4 | transition: opacity 0.2s ease-in-out;
5 | }
6 | .visit-item {
7 | position: relative;
8 | }
9 | .visit-item:hover .visit-checkbox-container {
10 | opacity: 1 !important;
11 | }
12 | .leaflet-drawer.open {
13 | transform: translateX(0);
14 | }
15 | .merge-visits-button {
16 | margin: 8px 0;
17 | }
18 |
--------------------------------------------------------------------------------
/app/jobs/app_version_checking_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AppVersionCheckingJob < ApplicationJob
4 | queue_as :default
5 | sidekiq_options retry: false
6 |
7 | def perform
8 | Rails.cache.delete(CheckAppVersion::VERSION_CACHE_KEY)
9 |
10 | CheckAppVersion.new.call
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/app/jobs/application_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationJob < ActiveJob::Base
4 | # Automatically retry jobs that encountered a deadlock
5 | # retry_on ActiveRecord::Deadlocked
6 |
7 | retry_on Exception, wait: :polynomially_longer, attempts: 25
8 |
9 | # Most jobs are safe to ignore if the underlying records are no longer available
10 | # discard_on ActiveJob::DeserializationError
11 | end
12 |
--------------------------------------------------------------------------------
/app/jobs/area_visits_calculating_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AreaVisitsCalculatingJob < ApplicationJob
4 | queue_as :default
5 | sidekiq_options retry: false
6 |
7 | def perform(user_id)
8 | user = User.find(user_id)
9 | areas = user.areas
10 |
11 | Areas::Visits::Create.new(user, areas).call
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/app/jobs/area_visits_calculation_scheduling_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AreaVisitsCalculationSchedulingJob < ApplicationJob
4 | queue_as :default
5 | sidekiq_options retry: false
6 |
7 | def perform
8 | User.find_each { AreaVisitsCalculatingJob.perform_later(_1.id) }
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/app/jobs/bulk_stats_calculating_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class BulkStatsCalculatingJob < ApplicationJob
4 | queue_as :stats
5 |
6 | def perform
7 | user_ids = User.pluck(:id)
8 |
9 | user_ids.each do |user_id|
10 | Stats::BulkCalculator.new(user_id).call
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/app/jobs/cache/cleaning_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Cache::CleaningJob < ApplicationJob
4 | queue_as :default
5 |
6 | def perform
7 | Cache::Clean.call
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/jobs/cache/preheating_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Cache::PreheatingJob < ApplicationJob
4 | queue_as :default
5 |
6 | def perform
7 | User.find_each do |user|
8 | Rails.cache.write(
9 | "dawarich/user_#{user.id}_years_tracked",
10 | user.years_tracked,
11 | expires_in: 1.day
12 | )
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/app/jobs/data_migrations/migrate_points_latlon_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class DataMigrations::MigratePointsLatlonJob < ApplicationJob
4 | queue_as :default
5 |
6 | def perform(user_id)
7 | user = User.find(user_id)
8 |
9 | # rubocop:disable Rails/SkipsModelValidations
10 | user.tracked_points.update_all('lonlat = ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)')
11 | # rubocop:enable Rails/SkipsModelValidations
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/app/jobs/data_migrations/set_points_country_ids_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class DataMigrations::SetPointsCountryIdsJob < ApplicationJob
4 | queue_as :default
5 |
6 | def perform(point_id)
7 | point = Point.find(point_id)
8 | country = Country.containing_point(point.lon, point.lat)
9 |
10 | if country.present?
11 | point.country_id = country.id
12 | point.save!
13 | else
14 | Rails.logger.info("No country found for point #{point.id}")
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/app/jobs/data_migrations/set_reverse_geocoded_at_for_points_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class DataMigrations::SetReverseGeocodedAtForPointsJob < ApplicationJob
4 | queue_as :default
5 |
6 | def perform
7 | timestamp = Time.current
8 |
9 | Point.where.not(geodata: {})
10 | .where(reverse_geocoded_at: nil)
11 | .in_batches(of: 10_000) do |relation|
12 | # rubocop:disable Rails/SkipsModelValidations
13 | relation.update_all(reverse_geocoded_at: timestamp)
14 | # rubocop:enable Rails/SkipsModelValidations
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/app/jobs/data_migrations/start_settings_points_country_ids_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class DataMigrations::StartSettingsPointsCountryIdsJob < ApplicationJob
4 | queue_as :default
5 |
6 | def perform
7 | Point.where(country_id: nil).find_each do |point|
8 | DataMigrations::SetPointsCountryIdsJob.perform_later(point.id)
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/jobs/enqueue_background_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class EnqueueBackgroundJob < ApplicationJob
4 | queue_as :reverse_geocoding
5 |
6 | def perform(job_name, user_id)
7 | case job_name
8 | when 'start_immich_import'
9 | Import::ImmichGeodataJob.perform_later(user_id)
10 | when 'start_photoprism_import'
11 | Import::PhotoprismGeodataJob.perform_later(user_id)
12 | when 'start_reverse_geocoding', 'continue_reverse_geocoding'
13 | Jobs::Create.new(job_name, user_id).call
14 | else
15 | raise ArgumentError, "Unknown job name: #{job_name}"
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/app/jobs/export_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ExportJob < ApplicationJob
4 | queue_as :exports
5 |
6 | def perform(export_id)
7 | export = Export.find(export_id)
8 |
9 | Exports::Create.new(export:).call
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/jobs/import/google_takeout_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Import::GoogleTakeoutJob < ApplicationJob
4 | queue_as :imports
5 | sidekiq_options retry: false
6 |
7 | def perform(import_id, locations, current_index)
8 | locations_batch = Oj.load(locations)
9 | import = Import.find(import_id)
10 |
11 | GoogleMaps::RecordsImporter.new(import, current_index).call(locations_batch)
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/app/jobs/import/immich_geodata_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Import::ImmichGeodataJob < ApplicationJob
4 | queue_as :imports
5 | sidekiq_options retry: false
6 |
7 | def perform(user_id)
8 | user = User.find(user_id)
9 |
10 | Immich::ImportGeodata.new(user).call
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/app/jobs/import/photoprism_geodata_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Import::PhotoprismGeodataJob < ApplicationJob
4 | queue_as :imports
5 | sidekiq_options retry: false
6 |
7 | def perform(user_id)
8 | user = User.find(user_id)
9 |
10 | Photoprism::ImportGeodata.new(user).call
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/app/jobs/import/process_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Import::ProcessJob < ApplicationJob
4 | queue_as :imports
5 |
6 | def perform(import_id)
7 | import = Import.find(import_id)
8 |
9 | import.process!
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/jobs/import/update_points_count_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Import::UpdatePointsCountJob < ApplicationJob
4 | queue_as :imports
5 |
6 | def perform(import_id)
7 | import = Import.find(import_id)
8 |
9 | import.update(processed: import.points.count)
10 | rescue ActiveRecord::RecordNotFound
11 | nil
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/app/jobs/import/watcher_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Import::WatcherJob < ApplicationJob
4 | queue_as :imports
5 | sidekiq_options retry: false
6 |
7 | def perform
8 | return unless DawarichSettings.self_hosted?
9 |
10 | Imports::Watcher.new.call
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/app/jobs/jobs/clean_finished_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Jobs::CleanFinishedJob < ApplicationJob
4 | queue_as :default
5 |
6 | def perform
7 | SolidQueue::Job.clear_finished_in_batches
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/jobs/overland/batch_creating_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Overland::BatchCreatingJob < ApplicationJob
4 | include PointValidation
5 |
6 | queue_as :points
7 |
8 | def perform(params, user_id)
9 | data = Overland::Params.new(params).call
10 |
11 | data.each do |location|
12 | next if location[:lonlat].nil?
13 | next if point_exists?(location, user_id)
14 |
15 | Point.create!(location.merge(user_id:))
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/app/jobs/owntracks/point_creating_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Owntracks::PointCreatingJob < ApplicationJob
4 | include PointValidation
5 |
6 | queue_as :points
7 |
8 | def perform(point_params, user_id)
9 | parsed_params = OwnTracks::Params.new(point_params).call
10 |
11 | return if parsed_params[:timestamp].nil? || parsed_params[:lonlat].nil?
12 | return if point_exists?(parsed_params, user_id)
13 |
14 | Point.create!(parsed_params.merge(user_id:))
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/app/jobs/points/create_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Points::CreateJob < ApplicationJob
4 | queue_as :points
5 |
6 | def perform(params, user_id)
7 | data = Points::Params.new(params, user_id).call
8 |
9 | data.each_slice(1000) do |location_batch|
10 | Point.upsert_all(
11 | location_batch,
12 | unique_by: %i[lonlat timestamp user_id],
13 | returning: false
14 | )
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/app/jobs/reverse_geocoding_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ReverseGeocodingJob < ApplicationJob
4 | queue_as :reverse_geocoding
5 |
6 | def perform(klass, id)
7 | return unless DawarichSettings.reverse_geocoding_enabled?
8 |
9 | rate_limit_for_photon_api
10 |
11 | data_fetcher(klass, id).call
12 | end
13 |
14 | private
15 |
16 | def data_fetcher(klass, id)
17 | "ReverseGeocoding::#{klass.pluralize.camelize}::FetchData".constantize.new(id)
18 | end
19 |
20 | def rate_limit_for_photon_api
21 | return unless DawarichSettings.photon_enabled?
22 |
23 | sleep 1 if DawarichSettings.photon_uses_komoot_io?
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/app/jobs/stats/calculating_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Stats::CalculatingJob < ApplicationJob
4 | queue_as :stats
5 |
6 | def perform(user_id, year, month)
7 | Stats::CalculateMonth.new(user_id, year, month).call
8 | rescue StandardError => e
9 | create_stats_update_failed_notification(user_id, e)
10 | end
11 |
12 | private
13 |
14 | def create_stats_update_failed_notification(user_id, error)
15 | user = User.find(user_id)
16 |
17 | Notifications::Create.new(
18 | user:,
19 | kind: :error,
20 | title: 'Stats update failed',
21 | content: "#{error.message}, stacktrace: #{error.backtrace.join("\n")}"
22 | ).call
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/app/jobs/trips/calculate_all_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Trips::CalculateAllJob < ApplicationJob
4 | queue_as :default
5 |
6 | def perform(trip_id, distance_unit = 'km')
7 | Trips::CalculatePathJob.perform_later(trip_id)
8 | Trips::CalculateDistanceJob.perform_later(trip_id, distance_unit)
9 | Trips::CalculateCountriesJob.perform_later(trip_id, distance_unit)
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/jobs/trips/calculate_countries_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Trips::CalculateCountriesJob < ApplicationJob
4 | queue_as :default
5 |
6 | def perform(trip_id, distance_unit)
7 | trip = Trip.find(trip_id)
8 |
9 | trip.calculate_countries
10 | trip.save!
11 |
12 | broadcast_update(trip, distance_unit)
13 | end
14 |
15 | private
16 |
17 | def broadcast_update(trip, distance_unit)
18 | Turbo::StreamsChannel.broadcast_update_to(
19 | "trip_#{trip.id}",
20 | target: "trip_countries",
21 | partial: "trips/countries",
22 | locals: { trip: trip, distance_unit: distance_unit }
23 | )
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/app/jobs/trips/calculate_distance_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Trips::CalculateDistanceJob < ApplicationJob
4 | queue_as :default
5 |
6 | def perform(trip_id, distance_unit)
7 | trip = Trip.find(trip_id)
8 |
9 | trip.calculate_distance
10 | trip.save!
11 |
12 | broadcast_update(trip, distance_unit)
13 | end
14 |
15 | private
16 |
17 | def broadcast_update(trip, distance_unit)
18 | Turbo::StreamsChannel.broadcast_update_to(
19 | "trip_#{trip.id}",
20 | target: "trip_distance",
21 | partial: "trips/distance",
22 | locals: { trip: trip, distance_unit: distance_unit }
23 | )
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/app/jobs/trips/calculate_path_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Trips::CalculatePathJob < ApplicationJob
4 | queue_as :default
5 |
6 | def perform(trip_id)
7 | trip = Trip.find(trip_id)
8 |
9 | trip.calculate_path
10 | trip.save!
11 |
12 | broadcast_update(trip)
13 | end
14 |
15 | private
16 |
17 | def broadcast_update(trip)
18 | Turbo::StreamsChannel.broadcast_update_to(
19 | "trip_#{trip.id}",
20 | target: "trip_path",
21 | partial: "trips/path",
22 | locals: { trip: trip }
23 | )
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/app/jobs/visit_suggesting_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class VisitSuggestingJob < ApplicationJob
4 | queue_as :visit_suggesting
5 | sidekiq_options retry: false
6 |
7 | # Passing timespan of more than 3 years somehow results in duplicated Places
8 | def perform(user_id:, start_at:, end_at:)
9 | user = User.find(user_id)
10 |
11 | start_time = parse_date(start_at)
12 | end_time = parse_date(end_at)
13 |
14 | # Create one-day chunks
15 | current_time = start_time
16 | while current_time < end_time
17 | chunk_end = [current_time + 1.day, end_time].min
18 | Visits::Suggest.new(user, start_at: current_time, end_at: chunk_end).call
19 | current_time += 1.day
20 | end
21 | end
22 |
23 | private
24 |
25 | def parse_date(date)
26 | date.is_a?(String) ? Time.zone.parse(date) : date.to_datetime
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/app/mailers/application_mailer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationMailer < ActionMailer::Base
4 | default from: ENV['SMTP_FROM']
5 | layout 'mailer'
6 | end
7 |
--------------------------------------------------------------------------------
/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | class ApplicationRecord < ActiveRecord::Base
2 | primary_abstract_class
3 | end
4 |
--------------------------------------------------------------------------------
/app/models/area.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Area < ApplicationRecord
4 | reverse_geocoded_by :latitude, :longitude
5 |
6 | belongs_to :user
7 | has_many :visits, dependent: :destroy
8 |
9 | validates :name, :latitude, :longitude, :radius, presence: true
10 |
11 | alias_attribute :lon, :longitude
12 | alias_attribute :lat, :latitude
13 |
14 | def center = [latitude.to_f, longitude.to_f]
15 | end
16 |
--------------------------------------------------------------------------------
/app/models/concerns/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/app/models/concerns/.keep
--------------------------------------------------------------------------------
/app/models/concerns/point_validation.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module PointValidation
4 | extend ActiveSupport::Concern
5 |
6 | def point_exists?(params, user_id)
7 | Point.where(
8 | lonlat: params[:lonlat],
9 | timestamp: params[:timestamp].to_i,
10 | user_id:
11 | ).exists?
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/app/models/country.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Country < ApplicationRecord
4 | validates :name, :iso_a2, :iso_a3, :geom, presence: true
5 |
6 | def self.containing_point(lon, lat)
7 | where("ST_Contains(geom, ST_SetSRID(ST_MakePoint(?, ?), 4326))", lon, lat)
8 | .select(:id, :name, :iso_a2, :iso_a3)
9 | .first
10 | end
11 |
12 | def self.names_to_iso_a2
13 | pluck(:name, :iso_a2).to_h
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/app/models/notification.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Notification < ApplicationRecord
4 | after_create_commit :broadcast_notification
5 |
6 | belongs_to :user
7 |
8 | validates :title, :content, :kind, presence: true
9 |
10 | enum :kind, { info: 0, warning: 1, error: 2 }
11 |
12 | scope :unread, -> { where(read_at: nil).order(created_at: :desc) }
13 |
14 | def read?
15 | read_at.present?
16 | end
17 |
18 | private
19 |
20 | def broadcast_notification
21 | NotificationsChannel.broadcast_to(
22 | user, {
23 | title: title,
24 | id: id,
25 | kind: kind
26 | }
27 | )
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/app/models/place.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Place < ApplicationRecord
4 | include Nearable
5 | include Distanceable
6 |
7 | DEFAULT_NAME = 'Suggested place'
8 |
9 | validates :name, :lonlat, presence: true
10 |
11 | has_many :visits, dependent: :destroy
12 | has_many :place_visits, dependent: :destroy
13 | has_many :suggested_visits, -> { distinct }, through: :place_visits, source: :visit
14 |
15 | enum :source, { manual: 0, photon: 1 }
16 |
17 | def lon
18 | lonlat.x
19 | end
20 |
21 | def lat
22 | lonlat.y
23 | end
24 |
25 | def osm_id
26 | geodata.dig('properties', 'osm_id')
27 | end
28 |
29 | def osm_key
30 | geodata.dig('properties', 'osm_key')
31 | end
32 |
33 | def osm_value
34 | geodata.dig('properties', 'osm_value')
35 | end
36 |
37 | def osm_type
38 | geodata.dig('properties', 'osm_type')
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/app/models/place_visit.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class PlaceVisit < ApplicationRecord
4 | belongs_to :place
5 | belongs_to :visit
6 | end
7 |
--------------------------------------------------------------------------------
/app/models/visit_draft.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class VisitDraft
4 | attr_accessor :start_time, :end_time, :points
5 |
6 | def initialize(start_time)
7 | @start_time = start_time
8 | @end_time = start_time
9 | @points = []
10 | end
11 |
12 | def add_point(point)
13 | @points << point
14 | @end_time = point.timestamp if point.timestamp > @end_time
15 | end
16 |
17 | def duration_in_minutes
18 | (end_time - start_time) / 60.0
19 | end
20 |
21 | def valid?
22 | @points.size > 1 && duration_in_minutes >= 10
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/app/policies/application_policy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationPolicy
4 | attr_reader :user, :record
5 |
6 | def initialize(user, record)
7 | @user = user
8 | @record = record
9 | end
10 |
11 | def index?
12 | false
13 | end
14 |
15 | def show?
16 | false
17 | end
18 |
19 | def create?
20 | false
21 | end
22 |
23 | def new?
24 | create?
25 | end
26 |
27 | def update?
28 | false
29 | end
30 |
31 | def edit?
32 | update?
33 | end
34 |
35 | def destroy?
36 | false
37 | end
38 |
39 | class Scope
40 | def initialize(user, scope)
41 | @user = user
42 | @scope = scope
43 | end
44 |
45 | def resolve
46 | raise NotImplementedError, "You must define #resolve in #{self.class}"
47 | end
48 |
49 | private
50 |
51 | attr_reader :user, :scope
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/app/serializers/api/place_serializer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Api::PlaceSerializer
4 | def initialize(place)
5 | @place = place
6 | end
7 |
8 | def call
9 | {
10 | id: place.id,
11 | name: place.name,
12 | longitude: place.lon,
13 | latitude: place.lat,
14 | city: place.city,
15 | country: place.country,
16 | source: place.source,
17 | geodata: place.geodata,
18 | created_at: place.created_at,
19 | updated_at: place.updated_at,
20 | reverse_geocoded_at: place.reverse_geocoded_at
21 | }
22 | end
23 |
24 | private
25 |
26 | attr_reader :place
27 | end
28 |
--------------------------------------------------------------------------------
/app/serializers/api/point_serializer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Api::PointSerializer < PointSerializer
4 | EXCLUDED_ATTRIBUTES = %w[created_at updated_at visit_id import_id user_id raw_data country_id].freeze
5 |
6 | def call
7 | point.attributes.except(*EXCLUDED_ATTRIBUTES)
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/serializers/api/slim_point_serializer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Api::SlimPointSerializer
4 | def initialize(point)
5 | @point = point
6 | end
7 |
8 | def call
9 | {
10 | id: point.id,
11 | latitude: point.lat.to_s,
12 | longitude: point.lon.to_s,
13 | timestamp: point.timestamp
14 | }
15 | end
16 |
17 | private
18 |
19 | attr_reader :point
20 | end
21 |
--------------------------------------------------------------------------------
/app/serializers/api/visit_serializer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Api::VisitSerializer
4 | def initialize(visit)
5 | @visit = visit
6 | end
7 |
8 | def call
9 | {
10 | id: visit.id,
11 | area_id: visit.area_id,
12 | user_id: visit.user_id,
13 | started_at: visit.started_at,
14 | ended_at: visit.ended_at,
15 | duration: visit.duration,
16 | name: visit.name,
17 | status: visit.status,
18 | place: {
19 | latitude: visit.place&.lat || visit.area&.latitude,
20 | longitude: visit.place&.lon || visit.area&.longitude,
21 | id: visit.place&.id
22 | }
23 | }
24 | end
25 |
26 | private
27 |
28 | attr_reader :visit
29 | end
30 |
--------------------------------------------------------------------------------
/app/serializers/point_serializer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class PointSerializer
4 | EXCLUDED_ATTRIBUTES = %w[
5 | created_at updated_at visit_id id import_id user_id raw_data lonlat
6 | reverse_geocoded_at country_id
7 | ].freeze
8 |
9 | def initialize(point)
10 | @point = point
11 | end
12 |
13 | def call
14 | point.attributes.except(*EXCLUDED_ATTRIBUTES).tap do |attributes|
15 | attributes['latitude'] = point.lat.to_s
16 | attributes['longitude'] = point.lon.to_s
17 | end
18 | end
19 |
20 | private
21 |
22 | attr_reader :point
23 | end
24 |
--------------------------------------------------------------------------------
/app/serializers/points/geojson_serializer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Points::GeojsonSerializer
4 | def initialize(points)
5 | @points = points
6 | end
7 |
8 | # rubocop:disable Metrics/MethodLength
9 | def call
10 | {
11 | type: 'FeatureCollection',
12 | features: points.map do |point|
13 | {
14 | type: 'Feature',
15 | geometry: {
16 | type: 'Point',
17 | coordinates: [point.lon, point.lat]
18 | },
19 | properties: PointSerializer.new(point).call
20 | }
21 | end
22 | }.to_json
23 | end
24 | # rubocop:enable Metrics/MethodLength
25 |
26 | private
27 |
28 | attr_reader :points
29 | end
30 |
--------------------------------------------------------------------------------
/app/serializers/points/gpx_serializer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Points::GpxSerializer
4 | def initialize(points, name)
5 | @points = points
6 | @name = name
7 | end
8 |
9 | def call
10 | gpx_file = GPX::GPXFile.new(name: "dawarich_#{name}")
11 | track = GPX::Track.new(name: "dawarich_#{name}")
12 |
13 | gpx_file.tracks << track
14 |
15 | track_segment = GPX::Segment.new
16 | track.segments << track_segment
17 |
18 | points.each do |point|
19 | track_segment.points << GPX::TrackPoint.new(
20 | lat: point.lat,
21 | lon: point.lon,
22 | elevation: point.altitude.to_f,
23 | time: point.recorded_at
24 | )
25 | end
26 |
27 | GPX::GPXFile.new(
28 | name: "dawarich_#{name}",
29 | gpx_data: gpx_file.to_s.sub(' e
13 | Rails.logger.error("Failed to send tile usage metric: #{e.message}")
14 | end
15 |
16 | private
17 |
18 | def report_to_prometheus
19 | return unless DawarichSettings.prometheus_exporter_enabled?
20 |
21 | metric_data = {
22 | type: 'counter',
23 | name: 'dawarich_map_tiles_usage',
24 | value: @count
25 | }
26 |
27 | PrometheusExporter::Client.default.send_json(metric_data)
28 | end
29 |
30 | def report_to_cache
31 | today_key = "dawarich_map_tiles_usage:#{@user_id}:#{Time.zone.today}"
32 |
33 | current_value = (Rails.cache.read(today_key) || 0).to_i
34 | Rails.cache.write(today_key, current_value + @count, expires_in: 7.days)
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/app/services/notifications/create.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Notifications::Create
4 | attr_reader :user, :kind, :title, :content
5 |
6 | def initialize(user:, kind:, title:, content:)
7 | @user = user
8 | @kind = kind
9 | @title = title
10 | @content = content
11 | end
12 |
13 | def call
14 | Notification.create!(user:, kind:, title:, content:)
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/app/services/own_tracks/rec_parser.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class OwnTracks::RecParser
4 | attr_reader :file
5 |
6 | def initialize(file)
7 | @file = file
8 | end
9 |
10 | def call
11 | file.split("\n").map do |line|
12 | parts = line.split("\t")
13 |
14 | Oj.load(parts[2]) if parts.size > 2 && parts[1].strip == '*'
15 | end.compact
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/app/services/photoprism/cache_preview_token.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Photoprism::CachePreviewToken
4 | attr_reader :user, :preview_token
5 |
6 | TOKEN_CACHE_KEY = 'dawarich/photoprism_preview_token'
7 |
8 | def initialize(user, preview_token)
9 | @user = user
10 | @preview_token = preview_token
11 | end
12 |
13 | def call
14 | Rails.cache.write("#{TOKEN_CACHE_KEY}_#{user.id}", preview_token)
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/app/services/photos/importer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Photos::Importer
4 | include Imports::Broadcaster
5 | include PointValidation
6 | attr_reader :import, :user_id
7 |
8 | def initialize(import, user_id)
9 | @import = import
10 | @user_id = user_id
11 | end
12 |
13 | def call
14 | file_content = Imports::SecureFileDownloader.new(import.file).download_with_verification
15 | json = Oj.load(file_content)
16 |
17 | json.each.with_index(1) { |point, index| create_point(point, index) }
18 | end
19 |
20 | def create_point(point, index)
21 | return 0 if point['latitude'].blank? || point['longitude'].blank? || point['timestamp'].blank?
22 | return 0 if point_exists?(point, point['timestamp'])
23 |
24 | Point.create(
25 | lonlat: "POINT(#{point['longitude']} #{point['latitude']})",
26 | timestamp: point['timestamp'],
27 | raw_data: point,
28 | import_id: import.id,
29 | user_id:
30 | )
31 |
32 | broadcast_import_progress(import, index)
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/app/services/points/create.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Points::Create
4 | attr_reader :user, :params
5 |
6 | def initialize(user, params)
7 | @user = user
8 | @params = params.to_h
9 | end
10 |
11 | def call
12 | data = Points::Params.new(params, user.id).call
13 |
14 | # Deduplicate points based on unique constraint
15 | deduplicated_data = data.uniq { |point| [point[:lonlat], point[:timestamp], point[:user_id]] }
16 |
17 | created_points = []
18 |
19 | deduplicated_data.each_slice(1000) do |location_batch|
20 | # rubocop:disable Rails/SkipsModelValidations
21 | result = Point.upsert_all(
22 | location_batch,
23 | unique_by: %i[lonlat timestamp user_id],
24 | returning: Arel.sql('id, timestamp, ST_X(lonlat::geometry) AS longitude, ST_Y(lonlat::geometry) AS latitude')
25 | )
26 | # rubocop:enable Rails/SkipsModelValidations
27 |
28 | created_points.concat(result)
29 | end
30 |
31 | created_points
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/app/services/points_limit_exceeded.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class PointsLimitExceeded
4 | def initialize(user)
5 | @user = user
6 | end
7 |
8 | def call
9 | return false if DawarichSettings.self_hosted?
10 | return true if @user.points.count >= points_limit
11 |
12 | false
13 | end
14 |
15 | private
16 |
17 | def points_limit
18 | DawarichSettings::BASIC_PAID_PLAN_LIMIT
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/app/services/reverse_geocoding/points/fetch_data.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ReverseGeocoding::Points::FetchData
4 | attr_reader :point
5 |
6 | def initialize(point_id)
7 | @point = Point.find(point_id)
8 | rescue ActiveRecord::RecordNotFound => e
9 | ExceptionReporter.call(e)
10 |
11 | Rails.logger.error("Point with id #{point_id} not found: #{e.message}")
12 | end
13 |
14 | def call
15 | return if point.reverse_geocoded?
16 |
17 | update_point_with_geocoding_data
18 | end
19 |
20 | private
21 |
22 | def update_point_with_geocoding_data
23 | response = Geocoder.search([point.lat, point.lon]).first
24 | return if response.blank? || response.data['error'].present?
25 |
26 | point.update!(
27 | city: response.city,
28 | country: response.country,
29 | geodata: response.data,
30 | reverse_geocoded_at: Time.current
31 | )
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/app/services/subscription/decode_jwt_token.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Subscription::DecodeJwtToken
4 | def initialize(token)
5 | @token = token
6 | end
7 |
8 | def call
9 | JWT.decode(
10 | @token,
11 | ENV['JWT_SECRET_KEY'],
12 | true,
13 | { algorithm: 'HS256' }
14 | ).first.symbolize_keys
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/app/services/tracks/build_path.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Tracks::BuildPath
4 | def initialize(coordinates)
5 | @coordinates = coordinates
6 | end
7 |
8 | def call
9 | factory.line_string(
10 | coordinates.map { |point| factory.point(point.lon.to_f.round(5), point.lat.to_f.round(5)) }
11 | )
12 | end
13 |
14 | private
15 |
16 | attr_reader :coordinates
17 |
18 | def factory
19 | @factory ||= RGeo::Geographic.spherical_factory(srid: 3857)
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/app/services/visits/find_in_time.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Visits
4 | class FindInTime
5 | def initialize(user, params)
6 | @user = user
7 | @start_at = parse_time(params[:start_at])
8 | @end_at = parse_time(params[:end_at])
9 | end
10 |
11 | def call
12 | Visit
13 | .includes(:place)
14 | .where(user:)
15 | .where('started_at >= ? AND ended_at <= ?', start_at, end_at)
16 | .order(started_at: :desc)
17 | end
18 |
19 | private
20 |
21 | attr_reader :user, :start_at, :end_at
22 |
23 | def parse_time(time_string)
24 | parsed_time = Time.zone.parse(time_string)
25 |
26 | raise ArgumentError, "Invalid time format: #{time_string}" if parsed_time.nil?
27 |
28 | parsed_time
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/app/services/visits/find_within_bounding_box.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Visits
4 | # Finds visits in a selected area on the map
5 | class FindWithinBoundingBox
6 | def initialize(user, params)
7 | @user = user
8 | @sw_lat = params[:sw_lat].to_f
9 | @sw_lng = params[:sw_lng].to_f
10 | @ne_lat = params[:ne_lat].to_f
11 | @ne_lng = params[:ne_lng].to_f
12 | end
13 |
14 | def call
15 | Visit
16 | .includes(:place)
17 | .where(user:)
18 | .joins(:place)
19 | .where(
20 | 'ST_Contains(ST_MakeEnvelope(?, ?, ?, ?, 4326), ST_SetSRID(places.lonlat::geometry, 4326))',
21 | sw_lng,
22 | sw_lat,
23 | ne_lng,
24 | ne_lat
25 | )
26 | .order(started_at: :desc)
27 | end
28 |
29 | private
30 |
31 | attr_reader :user, :sw_lat, :sw_lng, :ne_lat, :ne_lng
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/app/services/visits/finder.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Visits
4 | # Finds visits in a selected area on the map
5 | class Finder
6 | def initialize(user, params)
7 | @user = user
8 | @params = params
9 | end
10 |
11 | def call
12 | if area_selected?
13 | Visits::FindWithinBoundingBox.new(user, params).call
14 | else
15 | Visits::FindInTime.new(user, params).call
16 | end
17 | end
18 |
19 | private
20 |
21 | attr_reader :user, :params
22 |
23 | def area_selected?
24 | params[:selection] == 'true' &&
25 | params[:sw_lat].present? &&
26 | params[:sw_lng].present? &&
27 | params[:ne_lat].present? &&
28 | params[:ne_lng].present?
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/app/views/active_storage/blobs/_blob.html.erb:
--------------------------------------------------------------------------------
1 | attachment--<%= blob.filename.extension %>">
2 | <% if blob.representable? %>
3 | <%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %>
4 | <% end %>
5 |
6 |
7 | <% if caption = blob.try(:caption) %>
8 | <%= caption %>
9 | <% else %>
10 | <%= blob.filename %>
11 | <%= number_to_human_size blob.byte_size %>
12 | <% end %>
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/views/application/_favicon.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/views/devise/confirmations/new.html.erb:
--------------------------------------------------------------------------------
1 | Resend confirmation instructions
2 |
3 | <%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %>
4 | <%= render "devise/shared/error_messages", resource: resource %>
5 |
6 |
7 | <%= f.label :email %>
8 | <%= f.email_field :email, autofocus: true, autocomplete: "email", value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email) %>
9 |
10 |
11 |
12 | <%= f.submit "Resend confirmation instructions" %>
13 |
14 | <% end %>
15 |
16 | <%= render "devise/shared/links" %>
17 |
--------------------------------------------------------------------------------
/app/views/devise/mailer/confirmation_instructions.html.erb:
--------------------------------------------------------------------------------
1 | Welcome <%= @email %>!
2 |
3 | You can confirm your account email through the link below:
4 |
5 | <%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %>
6 |
--------------------------------------------------------------------------------
/app/views/devise/mailer/email_changed.html.erb:
--------------------------------------------------------------------------------
1 | Hello <%= @email %>!
2 |
3 | <% if @resource.try(:unconfirmed_email?) %>
4 | We're contacting you to notify you that your email is being changed to <%= @resource.unconfirmed_email %>.
5 | <% else %>
6 | We're contacting you to notify you that your email has been changed to <%= @resource.email %>.
7 | <% end %>
8 |
--------------------------------------------------------------------------------
/app/views/devise/mailer/password_change.html.erb:
--------------------------------------------------------------------------------
1 | Hello <%= @resource.email %>!
2 |
3 | We're contacting you to notify you that your password has been changed.
4 |
--------------------------------------------------------------------------------
/app/views/devise/mailer/reset_password_instructions.html.erb:
--------------------------------------------------------------------------------
1 | Hello <%= @resource.email %>!
2 |
3 | Someone has requested a link to change your password. You can do this through the link below.
4 |
5 | <%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %>
6 |
7 | If you didn't request this, please ignore this email.
8 | Your password won't change until you access the link above and create a new one.
9 |
--------------------------------------------------------------------------------
/app/views/devise/mailer/unlock_instructions.html.erb:
--------------------------------------------------------------------------------
1 | Hello <%= @resource.email %>!
2 |
3 | Your account has been locked due to an excessive number of unsuccessful sign in attempts.
4 |
5 | Click the link below to unlock your account:
6 |
7 | <%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %>
8 |
--------------------------------------------------------------------------------
/app/views/devise/passwords/edit.html.erb:
--------------------------------------------------------------------------------
1 | Change your password
2 |
3 | <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %>
4 | <%= render "devise/shared/error_messages", resource: resource %>
5 | <%= f.hidden_field :reset_password_token %>
6 |
7 |
8 | <%= f.label :password, "New password" %>
9 | <% if @minimum_password_length %>
10 | (<%= @minimum_password_length %> characters minimum)
11 | <% end %>
12 | <%= f.password_field :password, autofocus: true, autocomplete: "new-password" %>
13 |
14 |
15 |
16 | <%= f.label :password_confirmation, "Confirm new password" %>
17 | <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
18 |
19 |
20 |
21 | <%= f.submit "Change my password" %>
22 |
23 | <% end %>
24 |
25 | <%= render "devise/shared/links" %>
26 |
--------------------------------------------------------------------------------
/app/views/devise/passwords/new.html.erb:
--------------------------------------------------------------------------------
1 | Forgot your password?
2 |
3 | <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %>
4 | <%= render "devise/shared/error_messages", resource: resource %>
5 |
6 |
7 | <%= f.label :email %>
8 | <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
9 |
10 |
11 |
12 | <%= f.submit "Send me reset password instructions" %>
13 |
14 | <% end %>
15 |
16 | <%= render "devise/shared/links" %>
17 |
--------------------------------------------------------------------------------
/app/views/devise/registrations/_points_usage.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | You have used <%= number_with_delimiter(current_user.points.count) %> points of <%= number_with_delimiter(DawarichSettings::BASIC_PAID_PLAN_LIMIT) %> available.
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/views/devise/shared/_error_messages.html.erb:
--------------------------------------------------------------------------------
1 | <% if resource.errors.any? %>
2 |
3 |
4 | <%= I18n.t("errors.messages.not_saved",
5 | count: resource.errors.count,
6 | resource: resource.class.model_name.human.downcase)
7 | %>
8 |
9 |
10 | <% resource.errors.full_messages.each do |message| %>
11 | <%= message %>
12 | <% end %>
13 |
14 |
15 | <% end %>
16 |
--------------------------------------------------------------------------------
/app/views/devise/unlocks/new.html.erb:
--------------------------------------------------------------------------------
1 | Resend unlock instructions
2 |
3 | <%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %>
4 | <%= render "devise/shared/error_messages", resource: resource %>
5 |
6 |
7 | <%= f.label :email %>
8 | <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
9 |
10 |
11 |
12 | <%= f.submit "Resend unlock instructions" %>
13 |
14 | <% end %>
15 |
16 | <%= render "devise/shared/links" %>
17 |
--------------------------------------------------------------------------------
/app/views/imports/_import.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Name
6 | Imported points
7 | Created at
8 |
9 |
10 |
11 |
12 |
13 | <%= link_to import.name, import, class: 'underline hover:no-underline' %> (<%= import.source %>)
14 |
15 |
16 | <%= "#{number_with_delimiter import.points.size}" %>
17 |
18 | <%= human_datetime(import.created_at) %>
19 |
20 |
21 |
22 |
23 | <% if action_name != "show" %>
24 | <%= link_to "Show this import", import, class: "rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
25 | <%= link_to "Edit this import", edit_import_path(import), class: "rounded-lg py-3 ml-2 px-5 bg-gray-100 inline-block font-medium" %>
26 |
27 | <% end %>
28 |
29 |
--------------------------------------------------------------------------------
/app/views/imports/edit.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
Editing import
3 |
4 | <%= form_with model: @import, class: 'form-body mt-4' do |form| %>
5 |
6 | <%= form.label :name %>
7 | <%= form.text_field :name, class: 'input input-bordered' %>
8 |
9 |
10 |
11 | <%= form.label :source %>
12 | <%= form.select :source, options_for_select(Import.sources.keys.map { |source| [source.humanize, source] }, @import.source), {}, class: 'select select-bordered' %>
13 |
14 |
15 |
16 | <%= form.submit class: "rounded-lg py-3 px-5 bg-blue-600 text-white inline-block font-medium cursor-pointer" %>
17 | <%= link_to "Back to imports", imports_path, class: "rounded-lg py-3 px-5 bg-blue-600 text-white inline-block font-medium cursor-pointer" %>
18 |
19 | <% end %>
20 |
21 |
--------------------------------------------------------------------------------
/app/views/imports/new.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :title, 'New Import' %>
2 |
3 |
4 |
New import
5 |
6 | <%= render "form", import: @import %>
7 |
8 | <%= link_to "Back to imports", imports_path, class: "btn mx-5 mb-5" %>
9 |
10 |
--------------------------------------------------------------------------------
/app/views/imports/show.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :title, 'Import' %>
2 |
3 |
4 |
5 | <% if notice.present? %>
6 |
<%= notice %>
7 | <% end %>
8 |
9 | <%= render @import %>
10 |
11 | <%= link_to "Edit this import", edit_import_path(@import), class: "mt-2 rounded-lg py-3 px-5 bg-secondary-content inline-block font-medium" %>
12 |
13 | <%= link_to "Destroy this import", import_path(@import), data: { confirm: "Are you sure?", turbo_confirm: "Are you sure? This action will delete all points imported with this file", turbo_method: :delete }, method: :delete, class: "mt-2 rounded-lg py-3 px-5 bg-secondary-content font-medium" %>
14 |
15 | <%= link_to "Back to imports", imports_path, class: "ml-2 rounded-lg py-3 px-5 bg-secondary-content inline-block font-medium" %>
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/views/layouts/action_text/contents/_content.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= yield %>
3 |
4 |
--------------------------------------------------------------------------------
/app/views/layouts/mailer.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/views/layouts/mailer.text.erb:
--------------------------------------------------------------------------------
1 | <%= yield %>
2 |
--------------------------------------------------------------------------------
/app/views/notifications/show.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= render @notification %>
4 |
5 |
6 | <%= link_to "Back to notifications", notifications_path, class: "btn btn-small" %>
7 |
8 | <%= button_to "Destroy this notification", @notification, data: { confirm: "Are you sure?", turbo_confirm: "Are you sure?", turbo_method: :delete }, method: :delete, class: "btn btn-small btn-warning" %>
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/views/points/_point.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= check_box_tag "point_ids[]",
4 | point.id,
5 | nil,
6 | {
7 | multiple: true,
8 | form: :bulk_destroy_form,
9 | data: {
10 | checkbox_select_all_target: 'child',
11 | action: 'change->checkbox-select-all#toggleParent'
12 | }
13 | }
14 | %>
15 |
16 | <%= point.velocity %>
17 | <%= human_datetime_with_seconds(point.recorded_at) %>
18 | <%= point.lat %>, <%= point.lon %>
19 |
20 |
21 |
--------------------------------------------------------------------------------
/app/views/settings/_navigation.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= link_to 'Integrations', settings_path, role: 'tab', class: "tab tab-lg #{active_tab?(settings_path)}" %>
3 | <%= link_to 'Map', settings_maps_path, role: 'tab', class: "tab tab-lg #{active_tab?(settings_maps_path)}" %>
4 | <% if DawarichSettings.self_hosted? && current_user.admin? %>
5 | <%= link_to 'Users', settings_users_path, role: 'tab', class: "tab tab-lg #{active_tab?(settings_users_path)}" %>
6 | <%= link_to 'Background Jobs', settings_background_jobs_path, role: 'tab', class: "tab tab-lg #{active_tab?(settings_background_jobs_path)}" %>
7 | <% end %>
8 |
9 |
--------------------------------------------------------------------------------
/app/views/shared/_flash.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <% flash.each do |key, value| %>
3 |
6 |
<%= value %>
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | <% end %>
15 |
16 |
--------------------------------------------------------------------------------
/app/views/shared/_footer.html.erb:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/app/views/stats/_year.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= link_to year, "/stats/#{year}", class: "underline hover:no-underline text-#{header_colors.sample}" %>
3 | <%= link_to '[Map]', map_url(year_timespan(year)), class: 'underline hover:no-underline' %>
4 |
5 |
6 | <%= column_chart(
7 | Stat.year_distance(year, current_user),
8 | height: '200px',
9 | suffix: " #{current_user.safe_settings.distance_unit}",
10 | xtitle: 'Days',
11 | ytitle: 'Distance'
12 | ) %>
13 |
14 |
15 | <% stats.each do |stat| %>
16 | <%= render stat %>
17 | <% end %>
18 |
19 |
--------------------------------------------------------------------------------
/app/views/stats/show.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :title, "Statistics for #{@year} year" %>
2 |
3 |
4 | <%= render partial: 'stats/year', locals: { year: @year, stats: @stats } %>
5 |
6 |
--------------------------------------------------------------------------------
/app/views/trips/_distance.html.erb:
--------------------------------------------------------------------------------
1 | <% if trip.distance.present? %>
2 | <%= trip.distance %> <%= distance_unit %>
3 | <% else %>
4 | Calculating...
5 |
6 | <% end %>
7 |
--------------------------------------------------------------------------------
/app/views/trips/_path.html.erb:
--------------------------------------------------------------------------------
1 | <% if trip.path.present? %>
2 |
16 | <% else %>
17 |
18 |
19 |
Trip path is being calculated...
20 |
21 |
22 |
23 | <% end %>
24 |
--------------------------------------------------------------------------------
/app/views/trips/edit.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
Editing trip
3 |
4 | <%= render "form", trip: @trip %>
5 |
6 |
7 |
8 |
9 | <%= link_to "Show this trip", @trip, class: "btn" %>
10 | <%= link_to "Back to trips", trips_path, class: "btn" %>
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/views/trips/new.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :title, 'New trip' %>
2 |
3 |
4 |
New trip
5 |
6 | <%= render "form", trip: @trip %>
7 |
8 | <%= link_to "Back to trips", trips_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
9 |
10 |
--------------------------------------------------------------------------------
/app/views/visits/_buttons.html.erb:
--------------------------------------------------------------------------------
1 | <% if !visit.confirmed? %>
2 | <%= link_to 'Confirm', visit_path(visit, 'visit[status]': :confirmed), method: :patch, data: { turbo_method: :patch }, class: 'btn btn-xs btn-success' %>
3 | <% end %>
4 | <% if !visit.declined? %>
5 | <%= link_to 'Decline', visit_path(visit, 'visit[status]': :declined), method: :patch, data: { turbo_method: :patch }, class: 'btn btn-xs btn-error mx-1' %>
6 | <% end %>
7 |
--------------------------------------------------------------------------------
/app/views/visits/_name.html.erb:
--------------------------------------------------------------------------------
1 |
5 |
6 |
12 | <%= visit.default_name %>
13 |
14 |
15 |
16 |
23 |
24 |
--------------------------------------------------------------------------------
/app/views/visits/_visit.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= render 'visits/name', visit: visit %>
5 |
<%= "#{visit.started_at.strftime('%H:%M')} - #{visit.ended_at.strftime('%H:%M')}" %>
6 |
7 |
8 | <%= render 'visits/buttons', visit: visit %>
9 |
10 | Map
11 |
12 | <%= render 'visits/modal', visit: visit %>
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/bin/dev:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | if ! command -v foreman &> /dev/null
4 | then
5 | echo "Installing foreman..."
6 | gem install foreman
7 | fi
8 |
9 | if [ "$PROMETHEUS_EXPORTER_ENABLED" = "true" ]; then
10 | echo "Starting Foreman with Procfile.prometheus.dev..."
11 | foreman start -f Procfile.prometheus.dev
12 | else
13 | echo "Starting Foreman with Procfile.dev..."
14 | foreman start -f Procfile.dev
15 | fi
16 |
--------------------------------------------------------------------------------
/bin/importmap:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require_relative '../config/application'
4 | require 'importmap/commands'
5 |
--------------------------------------------------------------------------------
/bin/jobs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require_relative "../config/environment"
4 | require "solid_queue/cli"
5 |
6 | SolidQueue::Cli.start(ARGV)
7 |
--------------------------------------------------------------------------------
/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/rubocop:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require "rubygems"
3 | require "bundler/setup"
4 |
5 | # explicit rubocop config increases performance slightly while avoiding config confusion.
6 | ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__))
7 |
8 | load Gem.bin_path("rubocop", "rubocop")
9 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # This file is used by Rack-based servers to start the application.
4 |
5 | require_relative 'config/environment'
6 |
7 | run Rails.application
8 | Rails.application.load_server
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/cable.yml:
--------------------------------------------------------------------------------
1 | # Async adapter only works within the same process, so for manually triggering cable updates from a console,
2 | # and seeing results in the browser, you must do so from the web console (running inside the dev process),
3 | # not a terminal started via bin/rails console! Add "console" to any action or any ERB template view
4 | # to make the web console appear.
5 |
6 | default: &default
7 | adapter: solid_cable
8 | connects_to:
9 | database:
10 | writing: cable
11 | polling_interval: 0.1.seconds
12 | message_retention: 1.day
13 |
14 | development:
15 | <<: *default
16 |
17 | test:
18 | adapter: test
19 |
20 | production:
21 | <<: *default
22 |
--------------------------------------------------------------------------------
/config/cache.yml:
--------------------------------------------------------------------------------
1 | default: &default
2 | store_options:
3 | # Cap age of oldest cache entry to fulfill retention policies
4 | max_age: <%= 60.days.to_i %>
5 | max_size: <%= 256.megabytes %>
6 | namespace: <%= Rails.env %>
7 |
8 | development:
9 | <<: *default
10 |
11 | test:
12 | <<: *default
13 |
14 | production:
15 | <<: *default
16 |
--------------------------------------------------------------------------------
/config/database.ci.yml:
--------------------------------------------------------------------------------
1 | # config/database.ci.yml
2 | test:
3 | adapter: postgis
4 | encoding: unicode
5 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
6 | host: localhost
7 | database: <%= ENV["POSTGRES_DB"] %>
8 | username: <%= ENV['POSTGRES_USER'] %>
9 | password: <%= ENV["POSTGRES_PASSWORD"] %>
10 |
--------------------------------------------------------------------------------
/config/environment.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Load the Rails application.
4 | require_relative 'application'
5 |
6 | # Initialize the Rails application.
7 | Rails.application.initialize!
8 |
--------------------------------------------------------------------------------
/config/initializers/00_random.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # This code fixes failed to get urandom for running Ruby on Docker for Synology.
4 | class Random
5 | class << self
6 | private
7 |
8 | # :stopdoc:
9 |
10 | # Implementation using OpenSSL
11 | def gen_random_openssl(n)
12 | OpenSSL::Random.random_bytes(n)
13 | end
14 |
15 | begin
16 | # Check if Random.urandom is available
17 | Random.urandom(1)
18 | rescue RuntimeError
19 | begin
20 | require 'openssl'
21 | rescue NoMethodError
22 | raise NotImplementedError, 'No random device'
23 | else
24 | alias urandom gen_random_openssl
25 | end
26 | end
27 |
28 | public :urandom
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/config/initializers/assets.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # Version of your assets, change this if you want to expire all your assets.
6 | Rails.application.config.assets.version = '1.0'
7 |
8 | # Add additional assets to the asset load path.
9 | # Rails.application.config.assets.paths << Emoji.images_path
10 |
--------------------------------------------------------------------------------
/config/initializers/aws.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'aws-sdk-core'
4 |
5 | if ENV['AWS_ACCESS_KEY_ID'] &&
6 | ENV['AWS_SECRET_ACCESS_KEY'] &&
7 | ENV['AWS_REGION'] &&
8 | ENV['AWS_ENDPOINT']
9 | Aws.config.update(
10 | {
11 | region: ENV['AWS_REGION'],
12 | endpoint: ENV['AWS_ENDPOINT'],
13 | credentials: Aws::Credentials.new(ENV['AWS_ACCESS_KEY_ID'], ENV['AWS_SECRET_ACCESS_KEY'])
14 | }
15 | )
16 | end
17 |
--------------------------------------------------------------------------------
/config/initializers/cache_jobs.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Rails.application.config.after_initialize do
4 | # Only run in server mode and ensure one-time execution with atomic write
5 | if defined?(Rails::Server) && Rails.cache.write('cache_jobs_scheduled', true, unless_exist: true)
6 | # Clear the cache
7 | Cache::CleaningJob.perform_later
8 |
9 | # Preheat the cache
10 | Cache::PreheatingJob.perform_later
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file.
6 | # Use this to limit dissemination of sensitive information.
7 | # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors.
8 | Rails.application.config.filter_parameters += %i[
9 | passw secret token _key crypt salt certificate otp ssn cvv cvc latitude longitude lat lng
10 | ]
11 |
--------------------------------------------------------------------------------
/config/initializers/geocoder.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | settings = {
4 | timeout: 5,
5 | units: :km,
6 | cache: Redis.new,
7 | always_raise: :all,
8 | use_https: PHOTON_API_USE_HTTPS,
9 | http_headers: { 'User-Agent' => "Dawarich #{APP_VERSION} (https://dawarich.app)" },
10 | cache_options: {
11 | expiration: 1.day
12 | }
13 | }
14 |
15 | if PHOTON_API_HOST.present?
16 | settings[:lookup] = :photon
17 | settings[:photon] = { use_https: PHOTON_API_USE_HTTPS, host: PHOTON_API_HOST }
18 | settings[:http_headers] = { 'X-Api-Key' => PHOTON_API_KEY } if PHOTON_API_KEY.present?
19 | elsif GEOAPIFY_API_KEY.present?
20 | settings[:lookup] = :geoapify
21 | settings[:api_key] = GEOAPIFY_API_KEY
22 | elsif NOMINATIM_API_HOST.present?
23 | settings[:lookup] = :nominatim
24 | settings[:nominatim] = { use_https: NOMINATIM_API_USE_HTTPS, host: NOMINATIM_API_HOST }
25 | settings[:api_key] = NOMINATIM_API_KEY if NOMINATIM_API_KEY.present?
26 | end
27 |
28 | Geocoder.configure(settings)
29 |
--------------------------------------------------------------------------------
/config/initializers/httparty.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Suppress warnings about nil deprecation
4 | # https://github.com/jnunemaker/httparty/issues/568#issuecomment-1450473603
5 |
6 | HTTParty::Response.class_eval do
7 | def warn_about_nil_deprecation; end
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/mime_types.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Mime::Type.register 'application/geo+json', :geojson
4 |
--------------------------------------------------------------------------------
/config/initializers/permissions_policy.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Define an application-wide HTTP permissions policy. For further
4 | # information see: https://developers.google.com/web/updates/2018/06/feature-policy
5 |
6 | # Rails.application.config.permissions_policy do |policy|
7 | # policy.camera :none
8 | # policy.gyroscope :none
9 | # policy.microphone :none
10 | # policy.usb :none
11 | # policy.fullscreen :self
12 | # policy.payment :self, "https://secure.example.com"
13 | # end
14 |
--------------------------------------------------------------------------------
/config/initializers/prometheus.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | if !Rails.env.test? && DawarichSettings.prometheus_exporter_enabled?
4 | require 'prometheus_exporter/middleware'
5 | require 'prometheus_exporter/instrumentation'
6 |
7 | # This reports stats per request like HTTP status and timings
8 | Rails.application.middleware.unshift PrometheusExporter::Middleware
9 |
10 | # This reports basic process stats like RSS and GC info
11 | PrometheusExporter::Instrumentation::Process.start(type: 'web')
12 |
13 | # Add ActiveRecord instrumentation
14 | PrometheusExporter::Instrumentation::ActiveRecord.start
15 | end
16 |
--------------------------------------------------------------------------------
/config/initializers/rswag_api.rb:
--------------------------------------------------------------------------------
1 | Rswag::Api.configure do |c|
2 |
3 | # Specify a root folder where Swagger JSON files are located
4 | # This is used by the Swagger middleware to serve requests for API descriptions
5 | # NOTE: If you're using rswag-specs to generate Swagger, you'll need to ensure
6 | # that it's configured to generate files in the same folder
7 | c.openapi_root = Rails.root.to_s + '/swagger'
8 |
9 | # Inject a lambda function to alter the returned Swagger prior to serialization
10 | # The function will have access to the rack env for the current request
11 | # For example, you could leverage this to dynamically assign the "host" property
12 | #
13 | #c.swagger_filter = lambda { |swagger, env| swagger['host'] = env['HTTP_HOST'] }
14 | end
15 |
--------------------------------------------------------------------------------
/config/initializers/rswag_ui.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Rswag::Ui.configure do |c|
4 |
5 | # List the Swagger endpoints that you want to be documented through the
6 | # swagger-ui. The first parameter is the path (absolute or relative to the UI
7 | # host) to the corresponding endpoint and the second is a title that will be
8 | # displayed in the document selector.
9 | # NOTE: If you're using rspec-api to expose Swagger files
10 | # (under openapi_root) as JSON or YAML endpoints, then the list below should
11 | # correspond to the relative paths for those endpoints.
12 |
13 | c.openapi_endpoint '/api-docs/v1/swagger.yaml', 'API V1 Docs'
14 |
15 | # Add Basic Auth in case your API is private
16 | # c.basic_auth_enabled = true
17 | # c.basic_auth_credentials 'username', 'password'
18 | end
19 |
--------------------------------------------------------------------------------
/config/initializers/sentry.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | return unless SENTRY_DSN
4 |
5 | Sentry.init do |config|
6 | config.breadcrumbs_logger = [:active_support_logger]
7 | config.dsn = SENTRY_DSN
8 | config.traces_sample_rate = 1.0
9 | config.profiles_sample_rate = 1.0
10 | # config.enable_logs = true
11 | end
12 |
--------------------------------------------------------------------------------
/config/initializers/strong_migrations.rb:
--------------------------------------------------------------------------------
1 | # Mark existing migrations as safe
2 | StrongMigrations.start_after = 20_250_122_150_500
3 |
4 | # Set timeouts for migrations
5 | # If you use PgBouncer in transaction mode, delete these lines and set timeouts on the database user
6 | StrongMigrations.lock_timeout = 10.seconds
7 | StrongMigrations.statement_timeout = 1.hour
8 |
9 | # Analyze tables after indexes are added
10 | # Outdated statistics can sometimes hurt performance
11 | StrongMigrations.auto_analyze = true
12 |
13 | # Set the version of the production database
14 | # so the right checks are run in development
15 | # StrongMigrations.target_version = 10
16 |
17 | # Add custom checks
18 | # StrongMigrations.add_check do |method, args|
19 | # if method == :add_index && args[0].to_s == "users"
20 | # stop! "No more indexes on the users table"
21 | # end
22 | # end
23 |
24 | # Make some operations safe by default
25 | # See https://github.com/ankane/strong_migrations#safe-by-default
26 | # StrongMigrations.safe_by_default = true
27 |
--------------------------------------------------------------------------------
/config/initializers/web_app_manifest.rb:
--------------------------------------------------------------------------------
1 | # This file was generated by rails_favicon_generator, from
2 | # https://realfavicongenerator.net/
3 |
4 | # It makes files with .webmanifest extension first class files in the asset
5 | # pipeline. This is to preserve this extension, as is it referenced in a call
6 | # to asset_path in the _favicon.html.erb partial.
7 |
8 | Rails.application.config.assets.configure do |env|
9 | mime_type = 'application/manifest+json'
10 | extensions = ['.webmanifest']
11 |
12 | if Sprockets::VERSION.to_i >= 4
13 | extensions << '.webmanifest.erb'
14 | env.register_preprocessor(mime_type, Sprockets::ERBProcessor)
15 | end
16 |
17 | env.register_mime_type(mime_type, extensions: extensions)
18 | end
19 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 |
2 | en:
3 |
4 |
--------------------------------------------------------------------------------
/config/queue.yml:
--------------------------------------------------------------------------------
1 |
2 | default: &default
3 | dispatchers:
4 | - polling_interval: 1
5 | batch_size: 500
6 | workers:
7 | - queues: "*"
8 | threads: 3
9 | processes: <%= ENV['BACKGROUND_PROCESSING_CONCURRENCY'] || ENV.fetch("JOB_CONCURRENCY", 10) %>
10 | polling_interval: 2
11 | - queues: imports
12 | threads: 5
13 | processes: 1
14 | polling_interval: 1
15 | - queues: exports
16 | threads: 5
17 | processes: 1
18 | polling_interval: 2
19 |
20 | development:
21 | <<: *default
22 |
23 | test:
24 | <<: *default
25 |
26 | production:
27 | <<: *default
28 |
--------------------------------------------------------------------------------
/config/recurring.yml:
--------------------------------------------------------------------------------
1 | periodic_cleanup:
2 | class: "Jobs::CleanFinishedJob"
3 | queue: default
4 | schedule: every month
5 |
6 | bulk_stats_calculating_job:
7 | class: "BulkStatsCalculatingJob"
8 | queue: stats
9 | schedule: every hour
10 |
11 | area_visits_calculation_scheduling_job:
12 | class: "AreaVisitsCalculationSchedulingJob"
13 | queue: visit_suggesting
14 | schedule: every day at 0:00
15 |
16 | visit_suggesting_job:
17 | class: "BulkVisitsSuggestingJob"
18 | queue: visit_suggesting
19 | schedule: every day at 00:05
20 |
21 | watcher_job:
22 | class: "Import::WatcherJob"
23 | queue: imports
24 | schedule: every hour
25 |
26 | app_version_checking_job:
27 | class: "AppVersionCheckingJob"
28 | queue: default
29 | schedule: every 6 hours
30 |
31 | cache_preheating_job:
32 | class: "Cache::PreheatingJob"
33 | queue: default
34 | schedule: every day at 0:00
35 |
--------------------------------------------------------------------------------
/config/schedule.yml:
--------------------------------------------------------------------------------
1 | # config/schedule.yml
2 |
3 | bulk_stats_calculating_job:
4 | cron: "0 */1 * * *" # every 1 hour
5 | class: "BulkStatsCalculatingJob"
6 | queue: stats
7 |
8 | area_visits_calculation_scheduling_job:
9 | cron: "0 0 * * *" # every day at 0:00
10 | class: "AreaVisitsCalculationSchedulingJob"
11 | queue: visit_suggesting
12 |
13 | visit_suggesting_job:
14 | cron: "5 0 * * *" # every day at 00:05
15 | class: "BulkVisitsSuggestingJob"
16 | queue: visit_suggesting
17 |
18 | watcher_job:
19 | cron: "0 */1 * * *" # every 1 hour
20 | class: "Import::WatcherJob"
21 | queue: imports
22 |
23 | app_version_checking_job:
24 | cron: "0 */6 * * *" # every 6 hours
25 | class: "AppVersionCheckingJob"
26 | queue: default
27 |
28 | cache_preheating_job:
29 | cron: "0 0 * * *" # every day at 0:00
30 | class: "Cache::PreheatingJob"
31 | queue: default
32 |
--------------------------------------------------------------------------------
/config/sidekiq.yml:
--------------------------------------------------------------------------------
1 | ---
2 | :concurrency: <%= ENV.fetch("BACKGROUND_PROCESSING_CONCURRENCY", 10) %>
3 | :queues:
4 | - points
5 | - default
6 | - imports
7 | - exports
8 | - stats
9 | - reverse_geocoding
10 | - visit_suggesting
11 |
--------------------------------------------------------------------------------
/config/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const defaultTheme = require('tailwindcss/defaultTheme')
2 |
3 | module.exports = {
4 | content: [
5 | './app/helpers/**/*.rb',
6 | './app/javascript/**/*.js',
7 | './app/views/**/*.{erb,haml,html,slim}'
8 | ],
9 | theme: {
10 | extend: {
11 | fontFamily: {
12 | sans: ['Inter var', ...defaultTheme.fontFamily.sans],
13 | },
14 | },
15 | },
16 | plugins: [
17 | require('daisyui'),
18 | require('@tailwindcss/forms'),
19 | require('@tailwindcss/aspect-ratio'),
20 | require('@tailwindcss/typography'),
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/db/data/20240525110530_bind_existing_points_to_first_user.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class BindExistingPointsToFirstUser < ActiveRecord::Migration[7.1]
4 | def up
5 | user = User.first
6 |
7 | return if user.blank?
8 |
9 | points = Point.where(user_id: nil)
10 |
11 | points.update_all(user_id: user.id)
12 |
13 | Rails.logger.info "Bound #{points.count} points to user #{user.email}"
14 | end
15 |
16 | def down
17 | raise ActiveRecord::IrreversibleMigration
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/db/data/20240610170930_remove_points_without_coordinates.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class RemovePointsWithoutCoordinates < ActiveRecord::Migration[7.1]
4 | def up
5 | points = Point.where('longitude = 0.0 OR latitude = 0.0')
6 |
7 | Rails.logger.info "Found #{points.count} points without coordinates..."
8 |
9 | points
10 | .select { |point| point.raw_data['latitudeE7'].nil? && point.raw_data['longitudeE7'].nil? }
11 | .each(&:destroy)
12 |
13 | Rails.logger.info 'Points without coordinates removed.'
14 |
15 | BulkStatsCalculatingJob.perform_later
16 | end
17 |
18 | def down
19 | raise ActiveRecord::IrreversibleMigration
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/db/data/20240625201842_add_fog_of_war_meters_to_settings.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddFogOfWarMetersToSettings < ActiveRecord::Migration[7.1]
4 | def up
5 | User.find_each do |user|
6 | user.settings = user.settings.merge(fog_of_war_meters: 100)
7 | user.save!
8 | end
9 | end
10 |
11 | def down
12 | raise ActiveRecord::IrreversibleMigration
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/db/data/20240713103122_make_first_user_admin.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class MakeFirstUserAdmin < ActiveRecord::Migration[7.1]
4 | def up
5 | user = User.first
6 | user&.update!(admin: true)
7 | end
8 |
9 | def down
10 | user = User.first
11 | user&.update!(admin: false)
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/db/data/20240724141417_add_visit_settings_to_user.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddVisitSettingsToUser < ActiveRecord::Migration[7.1]
4 | def up
5 | User.find_each do |user|
6 | user.settings = user.settings.merge(
7 | time_threshold_minutes: 30,
8 | merge_threshold_minutes: 15
9 | )
10 | user.save!
11 | end
12 | end
13 |
14 | def down
15 | raise ActiveRecord::IrreversibleMigration
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/db/data/20240730130922_add_route_opacity_to_settings.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddRouteOpacityToSettings < ActiveRecord::Migration[7.1]
4 | def up
5 | User.find_each do |user|
6 | user.settings = user.settings.merge(route_opacity: 20)
7 | user.save!
8 | end
9 | end
10 |
11 | def down
12 | raise ActiveRecord::IrreversibleMigration
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/db/data/20240808133112_run_initial_visit_suggestion.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class RunInitialVisitSuggestion < ActiveRecord::Migration[7.1]
4 | def up
5 | start_at = 30.years.ago
6 | end_at = Time.current
7 |
8 | User.find_each do |user|
9 | VisitSuggestingJob.perform_later(user_id: user.id, start_at:, end_at:)
10 | end
11 | end
12 |
13 | def down
14 | raise ActiveRecord::IrreversibleMigration
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/db/data/20240822094532_add_counter_cache_to_imports.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddCounterCacheToImports < ActiveRecord::Migration[7.1]
4 | def up
5 | Import.find_each do |import|
6 | Import.reset_counters(import.id, :points)
7 | end
8 | end
9 |
10 | def down
11 | raise ActiveRecord::IrreversibleMigration
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/db/data/20241022100309_add_points_rendering_mode_to_settings.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddPointsRenderingModeToSettings < ActiveRecord::Migration[7.2]
4 | def up
5 | User.find_each do |user|
6 | user.settings = user.settings.merge(points_rendering_mode: 'raw')
7 | user.save!
8 | end
9 | end
10 |
11 | def down
12 | raise ActiveRecord::IrreversibleMigration
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/db/data/20241107112451_add_live_map_enabled_to_settings.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddLiveMapEnabledToSettings < ActiveRecord::Migration[7.2]
4 | def up
5 | User.find_each do |user|
6 | user.settings = user.settings.merge(live_map_enabled: false)
7 | user.save!
8 | end
9 | end
10 |
11 | def down
12 | raise ActiveRecord::IrreversibleMigration
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/db/data/20241202125248_set_reverse_geocoded_at_for_points.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class SetReverseGeocodedAtForPoints < ActiveRecord::Migration[7.2]
4 | def up
5 | DataMigrations::SetReverseGeocodedAtForPointsJob.perform_later
6 | end
7 |
8 | def down
9 | raise ActiveRecord::IrreversibleMigration
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/db/data/20241206163450_create_telemetry_notification.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class CreateTelemetryNotification < ActiveRecord::Migration[7.2]
4 | def up; end
5 |
6 | def down; end
7 | end
8 |
--------------------------------------------------------------------------------
/db/data/20250120154554_remove_duplicate_points.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class RemoveDuplicatePoints < ActiveRecord::Migration[8.0]
4 | def up
5 | # Find duplicate groups using a subquery
6 | duplicate_groups =
7 | Point.select('latitude, longitude, timestamp, user_id, COUNT(*) as count')
8 | .group('latitude, longitude, timestamp, user_id')
9 | .having('COUNT(*) > 1')
10 |
11 | puts "Duplicate groups found: #{duplicate_groups.length}"
12 |
13 | duplicate_groups.each do |group|
14 | points = Point.where(
15 | latitude: group.latitude,
16 | longitude: group.longitude,
17 | timestamp: group.timestamp,
18 | user_id: group.user_id
19 | ).order(id: :asc)
20 |
21 | # Keep the latest record and destroy all others
22 | latest = points.last
23 | points.where.not(id: latest.id).destroy_all
24 | end
25 | end
26 |
27 | def down
28 | # This migration cannot be reversed
29 | raise ActiveRecord::IrreversibleMigration
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/db/data/20250123151849_create_paths_for_trips.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class CreatePathsForTrips < ActiveRecord::Migration[8.0]
4 | def up
5 | Trip.find_each do |trip|
6 | Trips::CalculatePathJob.perform_later(trip.id)
7 | end
8 | end
9 |
10 | def down
11 | raise ActiveRecord::IrreversibleMigration
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/db/data/20250222213848_migrate_points_latlon.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class MigratePointsLatlon < ActiveRecord::Migration[8.0]
4 | def up
5 | User.find_each do |user|
6 | DataMigrations::MigratePointsLatlonJob.perform_later(user.id)
7 | end
8 | end
9 |
10 | def down
11 | raise ActiveRecord::IrreversibleMigration
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/db/data/20250226192005_activate_selfhosted_users.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ActivateSelfhostedUsers < ActiveRecord::Migration[8.0]
4 | def up
5 | return unless DawarichSettings.self_hosted?
6 |
7 | User.update_all(status: :active) # rubocop:disable Rails/SkipsModelValidations
8 | end
9 |
10 | def down
11 | return unless DawarichSettings.self_hosted?
12 |
13 | User.update_all(status: :inactive) # rubocop:disable Rails/SkipsModelValidations
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/db/data/20250303194123_migrate_places_lonlat.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class MigratePlacesLonlat < ActiveRecord::Migration[8.0]
4 | def up
5 | User.find_each do |user|
6 | DataMigrations::MigratePlacesLonlatJob.perform_later(user.id)
7 | end
8 | end
9 |
10 | def down
11 | raise ActiveRecord::IrreversibleMigration
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/db/data/20250403204658_update_imports_points_count.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class UpdateImportsPointsCount < ActiveRecord::Migration[8.0]
4 | def up
5 | Import.find_each do |import|
6 | Import::UpdatePointsCountJob.perform_later(import.id)
7 | end
8 | end
9 |
10 | def down
11 | raise ActiveRecord::IrreversibleMigration
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/db/data/20250404182629_set_active_until_for_selfhosted_users.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class SetActiveUntilForSelfhostedUsers < ActiveRecord::Migration[8.0]
4 | def up
5 | return unless DawarichSettings.self_hosted?
6 |
7 | # rubocop:disable Rails/SkipsModelValidations
8 | User.where(active_until: nil).update_all(active_until: 1000.years.from_now)
9 | # rubocop:enable Rails/SkipsModelValidations
10 | end
11 |
12 | def down
13 | return unless DawarichSettings.self_hosted?
14 |
15 | # rubocop:disable Rails/SkipsModelValidations
16 | User.where.not(active_until: nil).update_all(active_until: nil)
17 | # rubocop:enable Rails/SkipsModelValidations
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/db/data/20250516180933_set_points_country_ids.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class SetPointsCountryIds < ActiveRecord::Migration[8.0]
4 | def up
5 | DataMigrations::StartSettingsPointsCountryIdsJob.perform_later
6 | end
7 |
8 | def down
9 | raise ActiveRecord::IrreversibleMigration
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/db/data/20250518173936_fix_france_codes.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class FixFranceCodes < ActiveRecord::Migration[8.0]
4 | def up
5 | Country.find_by(name: 'France')&.update(iso_a2: 'FR', iso_a3: 'FRA')
6 | end
7 |
8 | def down
9 | raise ActiveRecord::IrreversibleMigration
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/db/data/20250518174305_set_default_distance_unit_for_user.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class SetDefaultDistanceUnitForUser < ActiveRecord::Migration[8.0]
4 | def up
5 | User.find_each do |user|
6 | map_settings = user.settings['maps']
7 |
8 | next if map_settings.try(:[], 'distance_unit')&.in?(%w[km mi])
9 |
10 | if map_settings.blank?
11 | map_settings = { distance_unit: 'km' }
12 | else
13 | map_settings['distance_unit'] = 'km'
14 | end
15 |
16 | user.settings['maps'] = map_settings
17 | user.save!
18 | end
19 | end
20 |
21 | def down
22 | raise ActiveRecord::IrreversibleMigration
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/db/data_schema.rb:
--------------------------------------------------------------------------------
1 | DataMigrate::Data.define(version: 20250518174305)
2 |
--------------------------------------------------------------------------------
/db/migrate/20231021104256_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/20231021104258_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb:
--------------------------------------------------------------------------------
1 | # This migration comes from active_storage (originally 20211119233751)
2 | class RemoveNotNullOnActiveStorageBlobsChecksum < ActiveRecord::Migration[6.0]
3 | def change
4 | return unless table_exists?(:active_storage_blobs)
5 |
6 | change_column_null(:active_storage_blobs, :checksum, true)
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/db/migrate/20240315215423_create_imports.rb:
--------------------------------------------------------------------------------
1 | class CreateImports < ActiveRecord::Migration[7.1]
2 | def change
3 | create_table :imports do |t|
4 | t.string :name, null: false
5 | t.bigint :user_id, null: false
6 | t.integer :source, default: 0
7 |
8 | t.timestamps
9 | end
10 | add_index :imports, :user_id
11 | add_index :imports, :source
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/db/migrate/20240317171559_add_indicies_to_points_latitude_longitude.rb:
--------------------------------------------------------------------------------
1 | class AddIndiciesToPointsLatitudeLongitude < ActiveRecord::Migration[7.1]
2 | def change
3 | add_index :points, [:latitude, :longitude]
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20240323125126_add_raw_points_and_doubles_to_import.rb:
--------------------------------------------------------------------------------
1 | class AddRawPointsAndDoublesToImport < ActiveRecord::Migration[7.1]
2 | def change
3 | add_column :imports, :raw_points, :integer, default: 0
4 | add_column :imports, :doubles, :integer, default: 0
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/db/migrate/20240323160300_create_stats.rb:
--------------------------------------------------------------------------------
1 | class CreateStats < ActiveRecord::Migration[7.1]
2 | def change
3 | create_table :stats do |t|
4 | t.integer :year, null: false
5 | t.integer :month, null: false
6 | t.integer :distance, null: false
7 | t.jsonb :toponyms
8 |
9 | t.timestamps
10 | end
11 | add_index :stats, :year
12 | add_index :stats, :month
13 | add_index :stats, :distance
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/db/migrate/20240323161049_add_index_to_points_timestamp.rb:
--------------------------------------------------------------------------------
1 | class AddIndexToPointsTimestamp < ActiveRecord::Migration[7.1]
2 | def change
3 | add_index :points, :timestamp
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20240323190039_add_user_id_to_stat.rb:
--------------------------------------------------------------------------------
1 | class AddUserIdToStat < ActiveRecord::Migration[7.1]
2 | def change
3 | add_reference :stats, :user, null: false, foreign_key: true
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20240324161800_add_processed_to_imports.rb:
--------------------------------------------------------------------------------
1 | class AddProcessedToImports < ActiveRecord::Migration[7.1]
2 | def change
3 | add_column :imports, :processed, :integer, default: 0
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20240324173315_add_daily_distance_to_stat.rb:
--------------------------------------------------------------------------------
1 | class AddDailyDistanceToStat < ActiveRecord::Migration[7.1]
2 | def change
3 | add_column :stats, :daily_distance, :jsonb, default: {}
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20240404154959_add_api_key_to_users.rb:
--------------------------------------------------------------------------------
1 | class AddApiKeyToUsers < ActiveRecord::Migration[7.1]
2 | def change
3 | add_column :users, :api_key, :string, null: false, default: ''
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20240425200155_add_raw_data_to_imports.rb:
--------------------------------------------------------------------------------
1 | class AddRawDataToImports < ActiveRecord::Migration[7.1]
2 | def change
3 | add_column :imports, :raw_data, :jsonb
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20240518095848_add_theme_to_users.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddThemeToUsers < ActiveRecord::Migration[7.1]
4 | def change
5 | add_column :users, :theme, :string, default: 'dark', null: false
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/db/migrate/20240525110244_add_user_id_to_points.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddUserIdToPoints < ActiveRecord::Migration[7.1]
4 | def change
5 | add_reference :points, :user, foreign_key: true
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/db/migrate/20240612152451_create_exports.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class CreateExports < ActiveRecord::Migration[7.1]
4 | def change
5 | create_table :exports do |t|
6 | t.string :name, null: false
7 | t.string :url
8 | t.integer :status, default: 0, null: false
9 | t.bigint :user_id, null: false
10 |
11 | t.timestamps
12 | end
13 | add_index :exports, :status
14 | add_index :exports, :user_id
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/db/migrate/20240620205120_add_settings_to_users.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddSettingsToUsers < ActiveRecord::Migration[7.1]
4 | def change
5 | add_column :users, :settings, :jsonb, default: {
6 | meters_between_routes: 500,
7 | minutes_between_routes: 60
8 | }
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/db/migrate/20240630093005_add_fog_of_war_to_default_settings.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddFogOfWarToDefaultSettings < ActiveRecord::Migration[7.1]
4 | def change
5 | change_column_default :users, :settings,
6 | from: { meters_between_routes: '1000', minutes_between_routes: '60' },
7 | to: { fog_of_war_meters: '100', meters_between_routes: '1000', minutes_between_routes: '60' }
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/db/migrate/20240703105734_create_notifications.rb:
--------------------------------------------------------------------------------
1 | class CreateNotifications < ActiveRecord::Migration[7.1]
2 | def change
3 | create_table :notifications do |t|
4 | t.string :title, null: false
5 | t.text :content, null: false
6 | t.references :user, null: false, foreign_key: true
7 | t.integer :kind, null: false, default: 0
8 | t.datetime :read_at
9 |
10 | t.timestamps
11 | end
12 | add_index :notifications, :kind
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/db/migrate/20240712141303_add_geodata_to_points.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddGeodataToPoints < ActiveRecord::Migration[7.1]
4 | def change
5 | add_column :points, :geodata, :jsonb, null: false, default: {}
6 | add_index :points, :geodata, using: :gin
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/db/migrate/20240713103051_add_admin_to_users.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddAdminToUsers < ActiveRecord::Migration[7.1]
4 | def change
5 | add_column :users, :admin, :boolean, default: false
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/db/migrate/20240721165313_create_areas.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class CreateAreas < ActiveRecord::Migration[7.1]
4 | def change
5 | create_table :areas do |t|
6 | t.string :name, null: false
7 | t.references :user, null: false, foreign_key: true
8 | t.decimal :longitude, precision: 10, scale: 6, null: false
9 | t.decimal :latitude, precision: 10, scale: 6, null: false
10 | t.integer :radius, null: false
11 |
12 | t.timestamps
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/db/migrate/20240721183005_create_visits.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class CreateVisits < ActiveRecord::Migration[7.1]
4 | def change
5 | create_table :visits do |t|
6 | t.references :area, null: false, foreign_key: true
7 | t.references :user, null: false, foreign_key: true
8 | t.datetime :started_at, null: false
9 | t.datetime :ended_at, null: false
10 | t.integer :duration, null: false
11 | t.string :name, null: false
12 | t.integer :status, null: false, default: 0
13 |
14 | t.timestamps
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/db/migrate/20240721183116_add_visit_id_to_points.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddVisitIdToPoints < ActiveRecord::Migration[7.1]
4 | def change
5 | add_reference :points, :visit, foreign_key: true
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/db/migrate/20240805150111_create_places.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class CreatePlaces < ActiveRecord::Migration[7.1]
4 | def change
5 | create_table :places do |t|
6 | t.string :name, null: false
7 | t.decimal :longitude, precision: 10, scale: 6, null: false
8 | t.decimal :latitude, precision: 10, scale: 6, null: false
9 | t.string :city
10 | t.string :country
11 | t.integer :source, default: 0
12 | t.jsonb :geodata, default: {}, null: false
13 | t.datetime :reverse_geocoded_at
14 |
15 | t.timestamps
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/db/migrate/20240808102348_add_place_id_to_visits.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddPlaceIdToVisits < ActiveRecord::Migration[7.1]
4 | def change
5 | add_reference :visits, :place, foreign_key: true
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/db/migrate/20240808102425_make_area_id_optional_in_visits.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class MakeAreaIdOptionalInVisits < ActiveRecord::Migration[7.1]
4 | def change
5 | change_column_null :visits, :area_id, true
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/db/migrate/20240808121027_create_place_visits.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class CreatePlaceVisits < ActiveRecord::Migration[7.1]
4 | def change
5 | create_table :place_visits do |t|
6 | t.references :place, null: false, foreign_key: true
7 | t.references :visit, null: false, foreign_key: true
8 |
9 | t.timestamps
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/db/migrate/20240822092405_add_points_count_to_imports.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddPointsCountToImports < ActiveRecord::Migration[7.1]
4 | def change
5 | add_column :imports, :points_count, :integer, default: 0
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/db/migrate/20241127161621_create_trips.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class CreateTrips < ActiveRecord::Migration[7.2]
4 | def change
5 | create_table :trips do |t|
6 | t.string :name, null: false
7 | t.datetime :started_at, null: false
8 | t.datetime :ended_at, null: false
9 | t.integer :distance
10 | t.references :user, null: false, foreign_key: true
11 |
12 | t.timestamps
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/db/migrate/20241202114820_add_reverse_geocoded_at_to_points.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddReverseGeocodedAtToPoints < ActiveRecord::Migration[7.2]
4 | disable_ddl_transaction!
5 |
6 | def change
7 | return if column_exists?(:points, :reverse_geocoded_at)
8 |
9 | add_column :points, :reverse_geocoded_at, :datetime
10 | add_index :points, :reverse_geocoded_at, algorithm: :concurrently
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/db/migrate/20241205160055_add_devise_trackable_columns_to_users.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddDeviseTrackableColumnsToUsers < ActiveRecord::Migration[7.2]
4 | def change
5 | change_table :users, bulk: true do |t|
6 | t.integer :sign_in_count, default: 0, null: false
7 | t.datetime :current_sign_in_at
8 | t.datetime :last_sign_in_at
9 | t.string :current_sign_in_ip
10 | t.string :last_sign_in_ip
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/db/migrate/20241211113119_add_started_at_index_to_visits.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddStartedAtIndexToVisits < ActiveRecord::Migration[7.2]
4 | disable_ddl_transaction!
5 |
6 | def change
7 | add_index :visits, :started_at, algorithm: :concurrently
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/db/migrate/20241226202204_add_database_users_constraints.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddDatabaseUsersConstraints < ActiveRecord::Migration[8.0]
4 | def change
5 | add_check_constraint :users, 'email IS NOT NULL', name: 'users_email_null', validate: false
6 | add_check_constraint :users, 'admin IS NOT NULL', name: 'users_admin_null', validate: false
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/db/migrate/20241226202831_validate_add_database_users_constraints.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ValidateAddDatabaseUsersConstraints < ActiveRecord::Migration[8.0]
4 | def up
5 | validate_check_constraint :users, name: 'users_email_null'
6 | change_column_null :users, :email, false
7 | remove_check_constraint :users, name: 'users_email_null'
8 | end
9 |
10 | def down
11 | add_check_constraint :users, 'email IS NOT NULL', name: 'users_email_null', validate: false
12 | change_column_null :users, :email, true
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/db/migrate/20250120152014_add_course_and_course_accuracy_to_points.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddCourseAndCourseAccuracyToPoints < ActiveRecord::Migration[8.0]
4 | def change
5 | add_column :points, :course, :decimal, precision: 8, scale: 5
6 | add_column :points, :course_accuracy, :decimal, precision: 8, scale: 5
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/db/migrate/20250120152540_add_external_track_id_to_points.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddExternalTrackIdToPoints < ActiveRecord::Migration[8.0]
4 | disable_ddl_transaction!
5 |
6 | def change
7 | add_column :points, :external_track_id, :string
8 |
9 | add_index :points, :external_track_id, algorithm: :concurrently
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/db/migrate/20250123145155_enable_postgis_extension.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class EnablePostgisExtension < ActiveRecord::Migration[8.0]
4 | def change
5 | enable_extension 'postgis' unless extension_enabled?('postgis')
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/db/migrate/20250123151657_add_path_to_trips.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddPathToTrips < ActiveRecord::Migration[8.0]
4 | def change
5 | add_column :trips, :path, :line_string, srid: 3857
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/db/migrate/20250219195822_add_status_to_users.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddStatusToUsers < ActiveRecord::Migration[8.0]
4 | def change
5 | add_column :users, :status, :integer, default: 0
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/db/migrate/20250221181805_add_lonlat_to_points.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddLonlatToPoints < ActiveRecord::Migration[8.0]
4 | def change
5 | add_column :points, :lonlat, :st_point, geographic: true
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/db/migrate/20250221185032_add_lonlat_index.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddLonlatIndex < ActiveRecord::Migration[8.0]
4 | disable_ddl_transaction!
5 |
6 | def change
7 | add_index :points, :lonlat, using: :gist, algorithm: :concurrently
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/db/migrate/20250221194430_remove_points_latitude_longitude_uniqueness_index.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class RemovePointsLatitudeLongitudeUniquenessIndex < ActiveRecord::Migration[8.0]
4 | disable_ddl_transaction!
5 |
6 | def up
7 | return unless index_exists?(
8 | :points, %i[latitude longitude timestamp user_id],
9 | name: 'unique_points_lat_long_timestamp_user_id_index'
10 | )
11 |
12 | remove_index :points,
13 | name: 'unique_points_lat_long_timestamp_user_id_index',
14 | algorithm: :concurrently
15 | end
16 |
17 | def down
18 | return if index_exists?(
19 | :points, %i[latitude longitude timestamp user_id],
20 | name: 'unique_points_lat_long_timestamp_user_id_index'
21 | )
22 |
23 | add_index :points, %i[latitude longitude timestamp user_id],
24 | unique: true,
25 | name: 'unique_points_lat_long_timestamp_user_id_index',
26 | algorithm: :concurrently
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/db/migrate/20250221194509_add_unique_lon_lat_index_to_points.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddUniqueLonLatIndexToPoints < ActiveRecord::Migration[8.0]
4 | disable_ddl_transaction!
5 |
6 | def change
7 | return if index_exists?(:points, %i[lonlat timestamp user_id], name: 'index_points_on_lonlat_timestamp_user_id')
8 |
9 | add_index :points, %i[lonlat timestamp user_id], unique: true,
10 | name: 'index_points_on_lonlat_timestamp_user_id',
11 | algorithm: :concurrently
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/db/migrate/20250303194009_add_lonlat_to_places.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddLonlatToPlaces < ActiveRecord::Migration[8.0]
4 | def change
5 | add_column :places, :lonlat, :st_point, geographic: true
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/db/migrate/20250303194043_add_lonlat_index_to_places.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddLonlatIndexToPlaces < ActiveRecord::Migration[8.0]
4 | disable_ddl_transaction!
5 |
6 | def change
7 | add_index :places, :lonlat, using: :gist, algorithm: :concurrently
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/db/migrate/20250324180755_add_format_start_at_end_at_to_exports.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddFormatStartAtEndAtToExports < ActiveRecord::Migration[8.0]
4 | def change
5 | add_column :exports, :file_format, :integer, default: 0
6 | add_column :exports, :start_at, :datetime
7 | add_column :exports, :end_at, :datetime
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/db/migrate/20250404182437_add_active_until_to_users.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddActiveUntilToUsers < ActiveRecord::Migration[8.0]
4 | def change
5 | add_column :users, :active_until, :datetime
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/db/migrate/20250513164521_add_visited_countries_to_trips.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddVisitedCountriesToTrips < ActiveRecord::Migration[8.0]
4 | def change
5 | safety_assured do
6 | execute <<-SQL
7 | ALTER TABLE trips ADD COLUMN visited_countries JSONB DEFAULT '{}'::jsonb NOT NULL;
8 | SQL
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/db/migrate/20250515190752_create_countries.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class CreateCountries < ActiveRecord::Migration[8.0]
4 | def change
5 | create_table :countries do |t|
6 | t.string :name, null: false
7 | t.string :iso_a2, null: false
8 | t.string :iso_a3, null: false
9 | t.multi_polygon :geom, srid: 4326
10 |
11 | t.timestamps
12 | end
13 |
14 | add_index :countries, :name
15 | add_index :countries, :iso_a2
16 | add_index :countries, :iso_a3
17 | add_index :countries, :geom, using: :gist
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/db/migrate/20250515192211_add_country_id_to_points.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddCountryIdToPoints < ActiveRecord::Migration[8.0]
4 | disable_ddl_transaction!
5 |
6 | def change
7 | add_reference :points, :country, index: { algorithm: :concurrently }
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/docker/.dockerignore:
--------------------------------------------------------------------------------
1 | /log
2 | /tmp
3 |
4 | # We need directories for import and export files, but not the files themselves.
5 | /public/exports/*
6 | !/public/exports/.keep
7 | /public/imports/*
8 | !/public/imports/.keep
9 |
--------------------------------------------------------------------------------
/docs/How_to_install_Dawarich_using_Docker.md:
--------------------------------------------------------------------------------
1 | # How to install Dawarich using Docker
2 |
3 | > To do that you need previously install [Docker](https://docs.docker.com/get-docker/) on your system.
4 |
5 | To quick Dawarich install copy the contents of the `docker-compose.yml` file from project root folder to dedicated folder in your server and run `docker compose up` in this folder.
6 |
7 | This command use [docker-compose.yml](../docker-compose.yml) to build your local environment.
8 |
9 | When this command done successfully and all services in containers will start you can open Dawarich web UI by this link [http://127.0.0.1:3000](http://127.0.0.1:3000).
10 |
11 | Default credentials for first login in are `demo@dawarich.app` and `password`.
12 |
--------------------------------------------------------------------------------
/docs/synology/.env:
--------------------------------------------------------------------------------
1 | ###################################################################################
2 | # Dawarich
3 | ###################################################################################
4 |
5 | RAILS_ENV=development
6 | MIN_MINUTES_SPENT_IN_CITY=60
7 | APPLICATION_HOSTS=dawarich.example.synology.me
8 | TIME_ZONE=Europe/Berlin
9 | BACKGROUND_PROCESSING_CONCURRENCY=10
10 | STORE_GEODATA=false
11 |
12 | ###################################################################################
13 | # Database
14 | ###################################################################################
15 |
16 | DATABASE_HOST=dawarich_db
17 | DATABASE_USERNAME=postgres
18 | DATABASE_PASSWORD=password
19 | DATABASE_NAME=dawarich
20 |
21 | ###################################################################################
22 | # Redis
23 | ###################################################################################
24 |
25 | REDIS_URL=redis://dawarich_redis:6379/0
26 |
--------------------------------------------------------------------------------
/docs/synology/spk.tgz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/docs/synology/spk.tgz
--------------------------------------------------------------------------------
/docs/synology/update.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | author="your name"
4 | URL="https://your.dawarich.domain.com"
5 |
6 |
7 | if [[ -d spk ]]
8 | then
9 | rm -rf spk
10 | fi
11 |
12 | if [[ -f Dawarich.spk ]]
13 | then
14 | rm -rf Dawarich.spk
15 | fi
16 |
17 | tar -xf spk.tgz
18 |
19 | if [[ -f package.tgz ]]
20 | then
21 | rm -f package.tgz
22 | fi
23 |
24 | sed -i "s/maintainer=\"\"/maintainer=\"${author}\"/" spk/INFO
25 | sed -i "s/distributor=\"\"/distributor=\"${author}\"/" spk/INFO
26 | sed -i "s|https://dawarich.my-syno.com|${URL}|" spk/package/ui/config
27 |
28 | cd spk/package
29 |
30 | tar -czf ../package.tgz *
31 |
32 | cd ..
33 |
34 | sum=$(md5sum package.tgz | cut -f1 -d" ")
35 |
36 | sed -i "s/checksum=\"\"/checksum=\"${sum}\"/" INFO
37 |
38 | tar -cf ../Dawarich.spk package.tgz conf scripts INFO PACKAGE_ICON*.PNG
39 |
40 | cd ..
41 |
--------------------------------------------------------------------------------
/lib/assets/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/lib/assets/.keep
--------------------------------------------------------------------------------
/lib/tasks/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/lib/tasks/.keep
--------------------------------------------------------------------------------
/lib/tasks/exports.rake:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | namespace :exports do
4 | desc 'Migrate existing exports from file system to the new file storage'
5 |
6 | task migrate_to_new_storage: :environment do
7 | Export.find_each do |export|
8 | export.migrate_to_new_storage
9 | rescue StandardError => e
10 | puts "Error migrating export #{export.id}: #{e.message}"
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/tasks/import.rake:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | namespace :import do
4 | # Usage: rake import:big_file['/path/to/file.json','user@email.com']
5 | desc 'Accepts a file path and user email and imports the data into the database'
6 |
7 | task :big_file, %i[file_path user_email] => :environment do |_, args|
8 | Tasks::Imports::GoogleRecords.new(args[:file_path], args[:user_email]).call
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/lib/tasks/imports.rake:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | namespace :imports do
4 | desc 'Migrate existing imports from `raw_data` to the new file storage'
5 |
6 | task migrate_to_new_storage: :environment do
7 | Import.find_each do |import|
8 | import.migrate_to_new_storage
9 | rescue StandardError => e
10 | puts "Error migrating import #{import.id}: #{e.message}"
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/tasks/rswag.rake:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | namespace :rswag do
4 | desc 'Generate Swagger docs'
5 | task generate: [:environment] do
6 | system 'bundle exec rake rswag:specs:swaggerize PATTERN="spec/swagger/**/*_spec.rb"'
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/tasks/users.rake:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | namespace :users do
4 | desc 'Activate all users'
5 | task activate: :environment do
6 | unless DawarichSettings.self_hosted?
7 | puts 'This task is only available for self-hosted users'
8 | exit 1
9 | end
10 |
11 | puts 'Activating all users...'
12 | # rubocop:disable Rails/SkipsModelValidations
13 | User.update_all(status: :active)
14 | # rubocop:enable Rails/SkipsModelValidations
15 |
16 | puts 'All users have been activated'
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/timestamps.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Timestamps
4 |
5 | def self.parse_timestamp(timestamp)
6 | begin
7 | # if the timestamp is in ISO 8601 format, try to parse it
8 | DateTime.parse(timestamp).to_time.to_i
9 | rescue
10 | if timestamp.to_s.length > 10
11 | # If the timestamp is in milliseconds, convert to seconds
12 | timestamp.to_i / 1000
13 | else
14 | # If the timestamp is in seconds, return it without change
15 | timestamp.to_i
16 | end
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/log/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/log/.keep
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "@hotwired/turbo-rails": "^7.3.0",
4 | "@rails/actiontext": "^8.0.0",
5 | "daisyui": "^4.7.3",
6 | "leaflet": "^1.9.4",
7 | "postcss": "^8.4.49",
8 | "trix": "^2.1.15"
9 | },
10 | "engines": {
11 | "node": "18.17.1",
12 | "npm": "9.6.7"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/public/.well-known/apple-app-site-association:
--------------------------------------------------------------------------------
1 | {
2 | "webcredentials": {
3 | "apps": [
4 | "2A275P77DQ.app.dawarich.Dawarich"
5 | ]
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/public/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/public/apple-touch-icon-precomposed.png
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/exports/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/public/exports/.keep
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/public/favicon.ico
--------------------------------------------------------------------------------
/public/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/public/icon.png
--------------------------------------------------------------------------------
/public/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 |
--------------------------------------------------------------------------------
/screenshots/imports.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/screenshots/imports.jpeg
--------------------------------------------------------------------------------
/screenshots/map.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/screenshots/map.jpeg
--------------------------------------------------------------------------------
/screenshots/stats.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/screenshots/stats.jpeg
--------------------------------------------------------------------------------
/spec/channels/imports_channel_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe ImportsChannel, type: :channel do
6 | let(:user) { create(:user) }
7 |
8 | before do
9 | stub_connection(current_user: user)
10 | end
11 |
12 | it 'subscribes to a stream for the current user' do
13 | subscribe
14 |
15 | expect(subscription).to be_confirmed
16 | expect(subscription).to have_stream_for(user)
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/spec/channels/notifications_channel_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe NotificationsChannel, type: :channel do
6 | let(:user) { create(:user) }
7 |
8 | before do
9 | stub_connection(current_user: user)
10 | end
11 |
12 | it 'subscribes to a stream for the current user' do
13 | subscribe
14 |
15 | expect(subscription).to be_confirmed
16 | expect(subscription).to have_stream_for(user)
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/spec/channels/points_channel_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe PointsChannel, type: :channel do
6 | let(:user) { create(:user) }
7 |
8 | before do
9 | stub_connection(current_user: user)
10 | end
11 |
12 | it 'subscribes to a stream for the current user' do
13 | subscribe
14 |
15 | expect(subscription).to be_confirmed
16 | expect(subscription).to have_stream_for(user)
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/spec/factories/areas.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :area do
5 | name { 'Adlershof' }
6 | user
7 | latitude { 52.437 }
8 | longitude { 13.539 }
9 | radius { 100 }
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/spec/factories/countries.rb:
--------------------------------------------------------------------------------
1 | FactoryBot.define do
2 | factory :country do
3 | name { "Serranilla Bank" }
4 | iso_a2 { "SB" }
5 | iso_a3 { "SBX" }
6 | geom {
7 | "MULTIPOLYGON (((-78.637074 15.862087, -78.640411 15.864, -78.636871 15.867296, -78.637074 15.862087)))"
8 | }
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/spec/factories/exports.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :export do
5 | name { 'export' }
6 | status { :created }
7 | file_format { :json }
8 | user
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/spec/factories/imports.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :import do
5 | user
6 | name { 'owntracks_export.json' }
7 | source { Import.sources[:owntracks] }
8 |
9 | trait :with_points do
10 | after(:create) do |import|
11 | create_list(:point, 10, import:)
12 | end
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/spec/factories/notifications.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :notification do
5 | title { "MyString" }
6 | content { "MyText" }
7 | user
8 | kind { :info }
9 | read_at { nil }
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/spec/factories/place_visits.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :place_visit do
5 | place
6 | visit
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/spec/factories/stats.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :stat do
5 | year { 1 }
6 | month { 1 }
7 | distance { 1 }
8 | user
9 | toponyms do
10 | [
11 | {
12 | 'cities' => [
13 | { 'city' => 'Moscow', 'points' => 7, 'timestamp' => 1_554_317_696, 'stayed_for' => 1831 }
14 | ],
15 | 'country' => 'Russia'
16 | }, { 'cities' => [], 'country' => nil }
17 | ]
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/spec/factories/trips.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :trip do
5 | user
6 | name { FFaker::Lorem.word }
7 | started_at { DateTime.new(2024, 11, 27, 17, 16, 21) }
8 | ended_at { DateTime.new(2024, 11, 29, 17, 16, 21) }
9 | notes { FFaker::Lorem.sentence }
10 | distance { 100 }
11 | path { 'LINESTRING(1 1, 2 2, 3 3)' }
12 |
13 | trait :with_points do
14 | after(:build) do |trip|
15 | (1..25).map do |i|
16 | create(
17 | :point,
18 | :with_geodata,
19 | :reverse_geocoded,
20 | timestamp: trip.started_at + i.minutes,
21 | user: trip.user
22 | )
23 | end
24 | end
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/spec/factories/visits.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :visit do
5 | area
6 | user
7 | started_at { Time.zone.now }
8 | ended_at { Time.zone.now + 1.hour }
9 | duration { 1.hour }
10 | name { 'Visit' }
11 | status { 'suggested' }
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/spec/fixtures/files/geojson/gpslogger_example.json:
--------------------------------------------------------------------------------
1 | {
2 | "features": [
3 | {
4 | "geometry": {
5 | "coordinates": [
6 | 106.64234449272531,
7 | 10.758321212464024
8 | ],
9 | "type": "Point"
10 | },
11 | "properties": {
12 | "accuracy": 4.7551565,
13 | "altitude": 17.634344400269068,
14 | "provider": "gps",
15 | "speed": 1.2,
16 | "time": "2024-11-03T16:30:11.331+07:00",
17 | "time_long": 1730626211331
18 | },
19 | "type": "Feature"
20 | }
21 | ],
22 | "type": "FeatureCollection"
23 | }
24 |
--------------------------------------------------------------------------------
/spec/fixtures/files/google/location-history/with_activitySegment_with_startLocation.json:
--------------------------------------------------------------------------------
1 | {
2 | "timelineObjects": [
3 | {
4 | "activitySegment": {
5 | "startLocation": { "latitudeE7": 123422222, "longitudeE7": 123422222 },
6 | "duration": { "startTimestamp": "2025-03-24 20:07:24 +0100" }
7 | }
8 | }
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/spec/fixtures/files/google/location-history/with_activitySegment_with_startLocation_timestampMs.json:
--------------------------------------------------------------------------------
1 | {
2 | "timelineObjects": [
3 | {
4 | "activitySegment": {
5 | "startLocation": { "latitudeE7": 123466666, "longitudeE7": 123466666 },
6 | "duration": { "startTimestampMs": "1742844302585" }
7 | }
8 | }
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/spec/fixtures/files/google/location-history/with_activitySegment_with_startLocation_timestamp_in_milliseconds_format.json:
--------------------------------------------------------------------------------
1 | {
2 | "timelineObjects": [
3 | {
4 | "activitySegment": {
5 | "startLocation": { "latitudeE7": 123455555, "longitudeE7": 123455555 },
6 | "duration": { "startTimestamp": "1742844232" }
7 | }
8 | }
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/spec/fixtures/files/google/location-history/with_activitySegment_with_startLocation_timestamp_in_seconds_format.json:
--------------------------------------------------------------------------------
1 | {
2 | "timelineObjects": [
3 | {
4 | "activitySegment": {
5 | "startLocation": { "latitudeE7": 123444444, "longitudeE7": 123444444 },
6 | "duration": { "startTimestamp": "1742844302585" }
7 | }
8 | }
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/spec/fixtures/files/google/location-history/with_activitySegment_with_startLocation_with_iso_timestamp.json:
--------------------------------------------------------------------------------
1 | {
2 | "timelineObjects": [
3 | {
4 | "activitySegment": {
5 | "startLocation": { "latitudeE7": 123433333, "longitudeE7": 123433333 },
6 | "duration": { "startTimestamp": "2025-03-24T20:20:23+01:00" }
7 | }
8 | }
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/spec/fixtures/files/google/location-history/with_activitySegment_without_startLocation.json:
--------------------------------------------------------------------------------
1 | {
2 | "timelineObjects": [
3 | {
4 | "activitySegment": {
5 | "waypointPath": {
6 | "waypoints": [
7 | { "latE7": 123411111, "lngE7": 123411111 }
8 | ]
9 | },
10 | "duration": { "startTimestamp": "2025-03-24 20:07:24 +0100" }
11 | }
12 | }
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/spec/fixtures/files/google/location-history/with_activitySegment_without_startLocation_without_waypointPath.json:
--------------------------------------------------------------------------------
1 | {
2 | "timelineObjects": [
3 | {
4 | "activitySegment": {
5 | "duration": { "startTimestamp": "2025-03-24 20:07:24 +0100" }
6 | }
7 | }
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/spec/fixtures/files/google/location-history/with_placeVisit_with_location_with_coordinates.json:
--------------------------------------------------------------------------------
1 | {
2 | "timelineObjects": [
3 | {
4 | "placeVisit": {
5 | "location": { "latitudeE7": 123477777, "longitudeE7": 123477777 },
6 | "duration": { "startTimestamp": "1742844232" }
7 | }
8 | }
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/spec/fixtures/files/google/location-history/with_placeVisit_with_location_with_coordinates_with_iso_timestamp.json:
--------------------------------------------------------------------------------
1 | {
2 | "timelineObjects": [
3 | {
4 | "placeVisit": {
5 | "location": { "latitudeE7": 123488888, "longitudeE7": 123488888 },
6 | "duration": { "startTimestamp": "2025-03-24T20:25:02+01:00" }
7 | }
8 | }
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/spec/fixtures/files/google/location-history/with_placeVisit_with_location_with_coordinates_with_milliseconds_timestamp.json:
--------------------------------------------------------------------------------
1 | {
2 | "timelineObjects": [
3 | {
4 | "placeVisit": {
5 | "location": { "latitudeE7": 123511111, "longitudeE7": 123511111 },
6 | "duration": { "startTimestamp": "1742844302585" }
7 | }
8 | }
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/spec/fixtures/files/google/location-history/with_placeVisit_with_location_with_coordinates_with_seconds_timestamp.json:
--------------------------------------------------------------------------------
1 | {
2 | "timelineObjects": [
3 | {
4 | "placeVisit": {
5 | "location": { "latitudeE7": 123499999, "longitudeE7": 123499999 },
6 | "duration": { "startTimestamp": "1742844302" }
7 | }
8 | }
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/spec/fixtures/files/google/location-history/with_placeVisit_with_location_with_coordinates_with_timestampMs.json:
--------------------------------------------------------------------------------
1 | {
2 | "timelineObjects": [
3 | {
4 | "placeVisit": {
5 | "location": { "latitudeE7": 123522222, "longitudeE7": 123522222 },
6 | "duration": { "startTimestampMs": "1742844302585" }
7 | }
8 | }
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/spec/fixtures/files/google/location-history/with_placeVisit_without_location_with_coordinates.json:
--------------------------------------------------------------------------------
1 | {
2 | "timelineObjects": [
3 | {
4 | "placeVisit": {
5 | "location": {},
6 | "duration": { "startTimestamp": "2025-03-24 20:25:02 +0100" }
7 | }
8 | }
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/spec/fixtures/files/google/location-history/with_placeVisit_without_location_with_coordinates_with_otherCandidateLocations.json:
--------------------------------------------------------------------------------
1 | {
2 | "timelineObjects": [
3 | {
4 | "placeVisit": {
5 | "otherCandidateLocations": [{ "latitudeE7": 123533333, "longitudeE7": 123533333 }],
6 | "duration": { "startTimestamp": "2025-03-24 20:25:02 +0100" }
7 | }
8 | }
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/spec/fixtures/files/google/records.json:
--------------------------------------------------------------------------------
1 | {
2 | "locations": [{
3 | "latitudeE7": 533690550,
4 | "longitudeE7": 836950010,
5 | "accuracy": 150,
6 | "source": "UNKNOWN",
7 | "timestamp": "2012-12-15T14:21:29.460Z"
8 | }, {
9 | "latitudeE7": 533563380,
10 | "longitudeE7": 837616500,
11 | "accuracy": 18000,
12 | "source": "UNKNOWN",
13 | "timestamp": "2013-01-04T10:22:43.225Z"
14 | }, {
15 | "latitudeE7": 533690589,
16 | "longitudeE7": 836951347,
17 | "accuracy": 22,
18 | "source": "WIFI",
19 | "deviceTag": 1184882232,
20 | "timestamp": "2013-03-01T05:17:39.849Z"
21 | }]
22 | }
23 |
--------------------------------------------------------------------------------
/spec/fixtures/files/immich/geodata.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "latitude": 59.0000,
4 | "longitude": 30.0000,
5 | "timestamp": 978296400
6 | },
7 | {
8 | "latitude": 55.0001,
9 | "longitude": 37.0001,
10 | "timestamp": 978296400
11 | }
12 | ]
13 |
--------------------------------------------------------------------------------
/spec/fixtures/files/immich/response.json:
--------------------------------------------------------------------------------
1 | [
2 | [
3 | {
4 | "assets": [
5 | {
6 | "exifInfo": {
7 | "dateTimeOriginal": "2022-12-31T23:17:06.170Z",
8 | "latitude": 52.0000,
9 | "longitude": 13.0000
10 | }
11 | },
12 | {
13 | "exifInfo": {
14 | "dateTimeOriginal": "2022-12-31T23:21:53.140Z",
15 | "latitude": 52.0000,
16 | "longitude": 13.0000
17 | }
18 | }
19 | ],
20 | "title": "1 year ago",
21 | "yearsAgo": 1
22 | }
23 | ]
24 | ]
25 |
--------------------------------------------------------------------------------
/spec/jobs/app_version_checking_job_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe AppVersionCheckingJob, type: :job do
6 | describe '#perform' do
7 | let(:job) { described_class.new }
8 |
9 | it 'calls CheckAppVersion service' do
10 | expect(CheckAppVersion).to receive(:new).and_return(instance_double(CheckAppVersion, call: true))
11 |
12 | job.perform
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/spec/jobs/area_visits_calculating_job_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe AreaVisitsCalculatingJob, type: :job do
6 | describe '#perform' do
7 | let(:user) { create(:user) }
8 | let(:area) { create(:area, user:) }
9 |
10 | it 'calls the AreaVisitsCalculationService' do
11 | expect(Areas::Visits::Create).to receive(:new).with(user, [area]).and_call_original
12 |
13 | described_class.new.perform(user.id)
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/spec/jobs/area_visits_calculation_scheduling_job_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe AreaVisitsCalculationSchedulingJob, type: :job do
6 | describe '#perform' do
7 | let(:area) { create(:area) }
8 | let(:user) { create(:user) }
9 |
10 | it 'calls the AreaVisitsCalculationService' do
11 | expect(AreaVisitsCalculatingJob).to receive(:perform_later).with(user.id).and_call_original
12 |
13 | described_class.new.perform
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/spec/jobs/bulk_stats_calculating_job_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe BulkStatsCalculatingJob, type: :job do
6 | describe '#perform' do
7 | let(:user1) { create(:user) }
8 | let(:user2) { create(:user) }
9 |
10 | let(:timestamp) { DateTime.new(2024, 1, 1).to_i }
11 |
12 | let!(:points1) do
13 | (1..10).map do |i|
14 | create(:point, user_id: user1.id, timestamp: timestamp + i.minutes)
15 | end
16 | end
17 |
18 | let!(:points2) do
19 | (1..10).map do |i|
20 | create(:point, user_id: user2.id, timestamp: timestamp + i.minutes)
21 | end
22 | end
23 |
24 | it 'enqueues Stats::CalculatingJob for each user' do
25 | expect(Stats::CalculatingJob).to receive(:perform_later).with(user1.id, 2024, 1)
26 | expect(Stats::CalculatingJob).to receive(:perform_later).with(user2.id, 2024, 1)
27 |
28 | BulkStatsCalculatingJob.perform_now
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/spec/jobs/data_migrations/migrate_points_latlon_job_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe DataMigrations::MigratePointsLatlonJob, type: :job do
6 | describe '#perform' do
7 | it 'updates the lonlat column for all tracked points' do
8 | user = create(:user)
9 | point = create(:point, latitude: 2.0, longitude: 1.0, user: user)
10 |
11 | expect { subject.perform(user.id) }.to change {
12 | point.reload.lonlat
13 | }.to(RGeo::Geographic.spherical_factory.point(1.0, 2.0))
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/spec/jobs/data_migrations/set_points_country_ids_job_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe DataMigrations::SetPointsCountryIdsJob, type: :job do
6 | describe '#perform' do
7 | let(:point) { create(:point, lonlat: 'POINT(10.0 20.0)', country_id: nil) }
8 | let(:country) { create(:country) }
9 |
10 | before do
11 | allow(Country).to receive(:containing_point)
12 | .with(point.lon, point.lat)
13 | .and_return(country)
14 | end
15 |
16 | it 'updates the point with the correct country_id' do
17 | described_class.perform_now(point.id)
18 |
19 | expect(point.reload.country_id).to eq(country.id)
20 | end
21 | end
22 |
23 | describe 'queue' do
24 | it 'uses the default queue' do
25 | expect(described_class.queue_name).to eq('default')
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/spec/jobs/enqueue_background_job_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe EnqueueBackgroundJob, type: :job do
6 | let(:job_name) { 'start_reverse_geocoding' }
7 | let(:user_id) { 1 }
8 |
9 | it 'calls job creation service' do
10 | expect(Jobs::Create).to receive(:new).with(job_name, user_id).and_return(double(call: nil))
11 |
12 | EnqueueBackgroundJob.perform_now(job_name, user_id)
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/spec/jobs/export_job_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe ExportJob, type: :job do
6 | let(:export) { create(:export) }
7 | let(:start_at) { 1.day.ago }
8 | let(:end_at) { Time.zone.now }
9 |
10 | it 'calls the Exports::Create service class' do
11 | expect(Exports::Create).to receive(:new).with(export:).and_call_original
12 |
13 | described_class.perform_now(export.id)
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/spec/jobs/import/immich_geodata_job_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe Import::ImmichGeodataJob, type: :job do
6 | describe '#perform' do
7 | let(:user) { create(:user) }
8 |
9 | it 'calls Immich::ImportGeodata' do
10 | expect_any_instance_of(Immich::ImportGeodata).to receive(:call)
11 |
12 | described_class.perform_now(user.id)
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/spec/jobs/import/watcher_job_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe Import::WatcherJob, type: :job do
6 | describe '#perform' do
7 | context 'when Dawarich is not self-hosted' do
8 | before do
9 | allow(DawarichSettings).to receive(:self_hosted?).and_return(false)
10 | end
11 |
12 | it 'does not call Imports::Watcher' do
13 | expect_any_instance_of(Imports::Watcher).not_to receive(:call)
14 |
15 | described_class.perform_now
16 | end
17 | end
18 |
19 | context 'when Dawarich is self-hosted' do
20 | before do
21 | allow(DawarichSettings).to receive(:self_hosted?).and_return(true)
22 | end
23 |
24 | it 'calls Imports::Watcher' do
25 | expect_any_instance_of(Imports::Watcher).to receive(:call)
26 |
27 | described_class.perform_now
28 | end
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/spec/jobs/overland/batch_creating_job_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe Overland::BatchCreatingJob, type: :job do
6 | describe '#perform' do
7 | subject(:perform) { described_class.new.perform(json, user.id) }
8 |
9 | let(:file_path) { 'spec/fixtures/files/overland/geodata.json' }
10 | let(:file) { File.open(file_path) }
11 | let(:json) { JSON.parse(file.read) }
12 | let(:user) { create(:user) }
13 |
14 | it 'creates a location' do
15 | expect { perform }.to change { Point.count }.by(1)
16 | end
17 |
18 | it 'creates a point with the correct user_id' do
19 | perform
20 |
21 | expect(Point.last.user_id).to eq(user.id)
22 | end
23 |
24 | context 'when point already exists' do
25 | it 'does not create a point' do
26 | perform
27 |
28 | expect { perform }.not_to(change { Point.count })
29 | end
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/spec/jobs/owntracks/point_creating_job_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe Owntracks::PointCreatingJob, type: :job do
6 | describe '#perform' do
7 | subject(:perform) { described_class.new.perform(point_params, user.id) }
8 |
9 | let(:point_params) do
10 | { lat: 1.0, lon: 1.0, tid: 'test', tst: Time.now.to_i, topic: 'iPhone 12 pro' }
11 | end
12 | let(:user) { create(:user) }
13 |
14 | it 'creates a point' do
15 | expect { perform }.to change { Point.count }.by(1)
16 | end
17 |
18 | it 'creates a point with the correct user_id' do
19 | perform
20 |
21 | expect(Point.last.user_id).to eq(user.id)
22 | end
23 |
24 | context 'when point already exists' do
25 | it 'does not create a point' do
26 | perform
27 |
28 | expect { perform }.not_to(change { Point.count })
29 | end
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/spec/jobs/points/create_job_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe Points::CreateJob, type: :job do
6 | describe '#perform' do
7 | subject(:perform) { described_class.new.perform(json, user.id) }
8 |
9 | let(:file_path) { 'spec/fixtures/files/points/geojson_example.json' }
10 | let(:file) { File.open(file_path) }
11 | let(:json) { JSON.parse(file.read) }
12 | let(:user) { create(:user) }
13 |
14 | it 'creates a point' do
15 | expect { perform }.to change { Point.count }.by(6)
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/spec/models/area_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe Area, type: :model do
6 | describe 'associations' do
7 | it { is_expected.to belong_to(:user) }
8 | it { is_expected.to have_many(:visits).dependent(:destroy) }
9 | end
10 |
11 | describe 'validations' do
12 | it { is_expected.to validate_presence_of(:name) }
13 | it { is_expected.to validate_presence_of(:latitude) }
14 | it { is_expected.to validate_presence_of(:longitude) }
15 | it { is_expected.to validate_presence_of(:radius) }
16 | end
17 |
18 | describe 'factory' do
19 | it { expect(build(:area)).to be_valid }
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/spec/models/country_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe Country, type: :model do
6 | describe 'validations' do
7 | it { is_expected.to validate_presence_of(:name) }
8 | it { is_expected.to validate_presence_of(:iso_a2) }
9 | it { is_expected.to validate_presence_of(:iso_a3) }
10 | it { is_expected.to validate_presence_of(:geom) }
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/spec/models/export_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe Export, type: :model do
6 | describe 'associations' do
7 | it { is_expected.to belong_to(:user) }
8 | end
9 |
10 | describe 'enums' do
11 | it { is_expected.to define_enum_for(:status).with_values(created: 0, processing: 1, completed: 2, failed: 3) }
12 | it { is_expected.to define_enum_for(:file_format).with_values(json: 0, gpx: 1) }
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/spec/models/notification_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe Notification, type: :model do
6 | describe 'validations' do
7 | it { is_expected.to validate_presence_of(:title) }
8 | it { is_expected.to validate_presence_of(:content) }
9 | it { is_expected.to validate_presence_of(:kind) }
10 | end
11 |
12 | describe 'associations' do
13 | it { is_expected.to belong_to(:user) }
14 | end
15 |
16 | describe 'enums' do
17 | it { is_expected.to define_enum_for(:kind).with_values(info: 0, warning: 1, error: 2) }
18 | end
19 |
20 | describe 'scopes' do
21 | describe '.unread' do
22 | let(:read_notification) { create(:notification, read_at: Time.current) }
23 | let(:unread_notification) { create(:notification, read_at: nil) }
24 |
25 | it 'returns only unread notifications' do
26 | expect(described_class.unread).to eq([unread_notification])
27 | end
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/spec/models/place_visit_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe PlaceVisit, type: :model do
6 | describe 'associations' do
7 | it { is_expected.to belong_to(:place) }
8 | it { is_expected.to belong_to(:visit) }
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/spec/models/visit_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe Visit, type: :model do
6 | describe 'associations' do
7 | it { is_expected.to belong_to(:area).optional }
8 | it { is_expected.to belong_to(:place).optional }
9 | it { is_expected.to belong_to(:user) }
10 | it { is_expected.to have_many(:points).dependent(:nullify) }
11 | end
12 |
13 | describe 'factory' do
14 | it { expect(build(:visit)).to be_valid }
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/spec/requests/api/v1/countries/borders_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe 'Api::V1::Countries::Borders', type: :request do
6 | describe 'GET /index' do
7 | it 'returns a list of countries with borders' do
8 | get '/api/v1/countries/borders'
9 |
10 | expect(response).to have_http_status(:success)
11 | expect(response.body).to include('AF')
12 | expect(response.body).to include('ZW')
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/spec/requests/api/v1/countries/visited_cities_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe 'Api::V1::Countries::VisitedCities', type: :request do
6 | describe 'GET /index' do
7 | let(:user) { create(:user) }
8 | let(:start_at) { '2023-01-01' }
9 | let(:end_at) { '2023-12-31' }
10 |
11 | it 'returns visited cities' do
12 | get "/api/v1/countries/visited_cities?api_key=#{user.api_key}&start_at=#{start_at}&end_at=#{end_at}"
13 |
14 | expect(response).to have_http_status(:ok)
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/spec/requests/api/v1/points/tracked_months_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe 'Api::V1::Points::TrackedMonths', type: :request do
6 | describe 'GET /index' do
7 | let(:user) { create(:user) }
8 |
9 | it 'returns tracked months' do
10 | get "/api/v1/points/tracked_months?api_key=#{user.api_key}"
11 |
12 | expect(response).to have_http_status(:ok)
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/spec/requests/api/v1/users_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe 'Api::V1::Users', type: :request do
6 | describe 'GET /me' do
7 | let(:user) { create(:user) }
8 | let(:headers) { { 'Authorization' => "Bearer #{user.api_key}" } }
9 |
10 | it 'returns http success' do
11 | get '/api/v1/users/me', headers: headers
12 |
13 | expect(response).to have_http_status(:success)
14 | expect(response.body).to include(user.email)
15 | expect(response.body).to include(user.id.to_s)
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/spec/requests/home_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe 'Homes', type: :request do
6 | describe 'GET /' do
7 | before do
8 | stub_request(:any, 'https://api.github.com/repos/Freika/dawarich/tags')
9 | .to_return(status: 200, body: '[{"name": "1.0.0"}]', headers: {})
10 | end
11 |
12 | it 'returns http success' do
13 | get '/'
14 |
15 | expect(response).to have_http_status(:success)
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/spec/requests/map_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe 'Map', type: :request do
6 | before do
7 | stub_request(:any, 'https://api.github.com/repos/Freika/dawarich/tags')
8 | .to_return(status: 200, body: '[{"name": "1.0.0"}]', headers: {})
9 | end
10 |
11 | describe 'GET /index' do
12 | context 'when user signed in' do
13 | let(:user) { create(:user) }
14 | let(:points) do
15 | (1..10).map do |i|
16 | create(:point, user:, timestamp: 1.day.ago + i.minutes)
17 | end
18 | end
19 |
20 | before { sign_in user }
21 |
22 | it 'returns http success' do
23 | get map_path
24 |
25 | expect(response).to have_http_status(:success)
26 | end
27 | end
28 |
29 | context 'when user not signed in' do
30 | it 'returns redirects to sign in page' do
31 | get map_path
32 |
33 | expect(response).to have_http_status(302)
34 | end
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/spec/requests/places_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe '/places', type: :request do
6 | let(:user) { create(:user) }
7 |
8 | before do
9 | stub_request(:any, 'https://api.github.com/repos/Freika/dawarich/tags')
10 | .to_return(status: 200, body: '[{"name": "1.0.0"}]', headers: {})
11 |
12 | sign_in user
13 | end
14 |
15 | describe 'GET /index' do
16 | it 'renders a successful response' do
17 | get places_url
18 |
19 | expect(response).to be_successful
20 | end
21 | end
22 |
23 | describe 'DELETE /destroy' do
24 | let!(:place) { create(:place) }
25 | let!(:visit) { create(:visit, place:, user:) }
26 |
27 | it 'destroys the requested place' do
28 | expect do
29 | delete place_url(place)
30 | end.to change(Place, :count).by(-1)
31 | end
32 |
33 | it 'redirects to the places list' do
34 | delete place_url(place)
35 |
36 | expect(response).to redirect_to(places_url)
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/spec/requests/users_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe 'Users', type: :request do
6 | before do
7 | stub_request(:any, 'https://api.github.com/repos/Freika/dawarich/tags')
8 | .to_return(status: 200, body: '[{"name": "1.0.0"}]', headers: {})
9 | end
10 |
11 | describe 'GET /users/sign_up' do
12 | context 'when self-hosted' do
13 | before do
14 | stub_const('SELF_HOSTED', true)
15 | end
16 |
17 | it 'returns http success' do
18 | get '/users/sign_up'
19 | expect(response).to have_http_status(:not_found)
20 | end
21 | end
22 |
23 | context 'when not self-hosted' do
24 | before do
25 | stub_const('SELF_HOSTED', false)
26 | Rails.application.reload_routes!
27 | end
28 |
29 | it 'returns http success' do
30 | get '/users/sign_up'
31 | expect(response).to have_http_status(:success)
32 | end
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/spec/serializers/api/point_serializer_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe Api::PointSerializer do
6 | describe '#call' do
7 | subject(:serializer) { described_class.new(point).call }
8 |
9 | let(:point) { create(:point) }
10 | let(:expected_json) { point.attributes.except(*Api::PointSerializer::EXCLUDED_ATTRIBUTES) }
11 |
12 | it 'returns JSON with correct attributes' do
13 | expect(serializer.to_json).to eq(expected_json.to_json)
14 | end
15 |
16 | it 'does not include excluded attributes' do
17 | expect(serializer).not_to include(*Api::PointSerializer::EXCLUDED_ATTRIBUTES)
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/spec/serializers/api/slim_point_serializer_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe Api::SlimPointSerializer do
6 | describe '#call' do
7 | subject(:serializer) { described_class.new(point).call }
8 |
9 | let!(:point) { create(:point, :with_known_location) }
10 | let(:expected_json) do
11 | {
12 | id: point.id,
13 | latitude: point.lat.to_s,
14 | longitude: point.lon.to_s,
15 | timestamp: point.timestamp
16 | }
17 | end
18 |
19 | it 'returns JSON with correct attributes' do
20 | expect(serializer.to_json).to eq(expected_json.to_json)
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/spec/serializers/points/geojson_serializer_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe Points::GeojsonSerializer do
6 | describe '#call' do
7 | subject(:serializer) { described_class.new(points).call }
8 |
9 | let(:points) do
10 | (1..3).map do |i|
11 | create(:point, timestamp: 1.day.ago + i.minutes)
12 | end
13 | end
14 |
15 | let(:expected_json) do
16 | {
17 | type: 'FeatureCollection',
18 | features: points.map do |point|
19 | {
20 | type: 'Feature',
21 | geometry: {
22 | type: 'Point',
23 | coordinates: [point.lon, point.lat]
24 | },
25 | properties: PointSerializer.new(point).call
26 | }
27 | end
28 | }
29 | end
30 |
31 | it 'returns JSON' do
32 | expect(serializer).to eq(expected_json.to_json)
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/spec/serializers/points/gpx_serializer_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe Points::GpxSerializer do
6 | describe '#call' do
7 | subject(:serializer) { described_class.new(points, 'some_name').call }
8 |
9 | let(:points) do
10 | (1..3).map do |i|
11 | create(:point, timestamp: 1.day.ago + i.minutes)
12 | end
13 | end
14 |
15 | it 'returns GPX file' do
16 | expect(serializer).to be_a(GPX::GPXFile)
17 | end
18 |
19 | it 'includes waypoints' do
20 | expect(serializer.tracks[0].points.size).to eq(3)
21 | end
22 |
23 | it 'includes waypoints with correct attributes' do
24 | serializer.tracks[0].points.each_with_index do |track_point, index|
25 | point = points[index]
26 |
27 | expect(track_point.lat.to_s).to eq(point.lat.to_s)
28 | expect(track_point.lon.to_s).to eq(point.lon.to_s)
29 | expect(track_point.time).to eq(point.recorded_at)
30 | end
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/spec/services/geojson/importer_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe Geojson::Importer do
6 | describe '#call' do
7 | subject(:service) { described_class.new(import, user.id).call }
8 |
9 | let(:user) { create(:user) }
10 |
11 | let(:user) { create(:user) }
12 |
13 | context 'when file content is an object' do
14 | let(:file_path) { Rails.root.join('spec/fixtures/files/geojson/export.json') }
15 | let(:file) { Rack::Test::UploadedFile.new(file_path, 'application/json') }
16 | let(:import) { create(:import, user:, name: 'geojson.json', file:) }
17 |
18 | before do
19 | import.file.attach(io: File.open(file_path), filename: 'geojson.json', content_type: 'application/json')
20 | end
21 |
22 | it 'creates new points' do
23 | expect { service }.to change { Point.count }.by(10)
24 | end
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/spec/services/imports/destroy_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe Imports::Destroy do
6 | describe '#call' do
7 | let!(:user) { create(:user) }
8 | let!(:import) { create(:import, :with_points, user: user) }
9 | let(:service) { described_class.new(user, import) }
10 |
11 | it 'destroys the import' do
12 | expect { service.call }.to change { Import.count }.by(-1)
13 | end
14 |
15 | it 'destroys the points' do
16 | expect { service.call }.to change { Point.count }.by(-import.points.count)
17 | end
18 |
19 | it 'enqueues a BulkStatsCalculatingJob' do
20 | expect(Stats::BulkCalculator).to receive(:new).with(user.id).and_return(double(call: nil))
21 |
22 | service.call
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/spec/services/notifications/create_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe Notifications::Create do
6 | describe '#call' do
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/spec/services/photoprism/cache_preview_token_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe Photoprism::CachePreviewToken, type: :service do
6 | let(:user) { double('User', id: 1) }
7 | let(:preview_token) { 'sample_token' }
8 | let(:service) { described_class.new(user, preview_token) }
9 |
10 | describe '#call' do
11 | it 'writes the preview token to the cache with the correct key' do
12 | expect(Rails.cache).to receive(:write).with(
13 | "dawarich/photoprism_preview_token_#{user.id}", preview_token
14 | )
15 |
16 | service.call
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/spec/services/reverse_geocoding/places/fetch_data_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe ReverseGeocoding::Places::FetchData do
6 | describe '#call' do
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/spec/services/tasks/imports/google_records_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe Tasks::Imports::GoogleRecords do
6 | describe '#call' do
7 | let(:user) { create(:user) }
8 | let(:file_path) { Rails.root.join('spec/fixtures/files/google/records.json').to_s }
9 |
10 | it 'schedules the Import::GoogleTakeoutJob' do
11 | expect(Import::GoogleTakeoutJob).to receive(:perform_later).exactly(1).time
12 |
13 | described_class.new(file_path, user.email).call
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/spec/support/devise.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # https://makandracards.com/makandra/37161-rspec-devise-how-to-sign-in-users-in-request-specs
4 |
5 | module DeviseRequestSpecHelpers
6 | include Warden::Test::Helpers
7 |
8 | def sign_in(resource_or_scope, resource = nil)
9 | resource ||= resource_or_scope
10 | scope = Devise::Mapping.find_scope!(resource_or_scope)
11 | login_as(resource, scope:)
12 | end
13 |
14 | def sign_out(resource_or_scope)
15 | scope = Devise::Mapping.find_scope!(resource_or_scope)
16 | logout(scope)
17 | end
18 | end
19 |
20 | RSpec.configure do |config|
21 | config.include DeviseRequestSpecHelpers, type: :request
22 | end
23 |
--------------------------------------------------------------------------------
/spec/support/geocoder_stubs.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Stub all Geocoder requests in tests
4 | RSpec.configure do |config|
5 | config.before(:each) do
6 | # Create a generic stub for all Geocoder requests
7 | allow(Geocoder).to receive(:search).and_return(
8 | [
9 | double(
10 | data: {
11 | 'properties' => {
12 | 'countrycode' => 'US',
13 | 'country' => 'United States',
14 | 'state' => 'New York',
15 | 'name' => 'Test Location'
16 | }
17 | }
18 | )
19 | ]
20 | )
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/spec/support/pundit_matchers.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Custom RSpec matchers for Pundit policies
4 |
5 | RSpec::Matchers.define :permit do |action|
6 | match do |policy|
7 | policy.public_send("#{action}?")
8 | end
9 |
10 | failure_message do |policy|
11 | "#{policy.class} does not permit #{action} on #{policy.record} for #{policy.user.inspect}."
12 | end
13 |
14 | failure_message_when_negated do |policy|
15 | "#{policy.class} does not forbid #{action} on #{policy.record} for #{policy.user.inspect}."
16 | end
17 | end
18 |
19 | RSpec::Matchers.define :forbid do |action|
20 | match do |policy|
21 | policy.public_send("#{action}?")
22 | end
23 |
24 | failure_message do |policy|
25 | "#{policy.class} does not forbid #{action} on #{policy.record} for #{policy.user.inspect}."
26 | end
27 |
28 | failure_message_when_negated do |policy|
29 | "#{policy.class} does not permit #{action} on #{policy.record} for #{policy.user.inspect}."
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/spec/support/system_helpers.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SystemHelpers
4 | def sign_in_user(user, password = 'password123')
5 | visit new_user_session_path
6 | fill_in 'Email', with: user.email
7 | fill_in 'Password', with: password
8 | click_button 'Log in'
9 | end
10 |
11 | def sign_in_and_visit_map(user, password = 'password123')
12 | sign_in_user(user, password)
13 | expect(page).to have_current_path(map_path)
14 | expect(page).to have_css('.leaflet-container', wait: 10)
15 | end
16 | end
17 |
18 | RSpec.configure do |config|
19 | config.include SystemHelpers, type: :system
20 | end
21 |
--------------------------------------------------------------------------------
/spec/tasks/import_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | describe 'import.rake' do
6 | let(:file_path) { Rails.root.join('spec/fixtures/files/google/records.json').to_s }
7 | let(:user) { create(:user) }
8 |
9 | it 'calls importing class' do
10 | expect(Tasks::Imports::GoogleRecords).to receive(:new).with(file_path, user.email).and_call_original.once
11 |
12 | Rake::Task['import:big_file'].invoke(file_path, user.email)
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/storage/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/storage/.keep
--------------------------------------------------------------------------------
/test/application_system_test_case.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
4 | driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
5 | end
6 |
--------------------------------------------------------------------------------
/test/channels/application_cable/connection_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase
4 | # test "connects with cookies" do
5 | # cookies.signed[:user_id] = 42
6 | #
7 | # connect
8 | #
9 | # assert_equal connection.user_id, "42"
10 | # end
11 | end
12 |
--------------------------------------------------------------------------------
/test/controllers/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/test/controllers/.keep
--------------------------------------------------------------------------------
/test/fixtures/files/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/test/fixtures/files/.keep
--------------------------------------------------------------------------------
/test/helpers/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/test/helpers/.keep
--------------------------------------------------------------------------------
/test/integration/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/test/integration/.keep
--------------------------------------------------------------------------------
/test/mailers/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/test/mailers/.keep
--------------------------------------------------------------------------------
/test/models/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/test/models/.keep
--------------------------------------------------------------------------------
/test/system/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/test/system/.keep
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | ENV["RAILS_ENV"] ||= "test"
2 | require_relative "../config/environment"
3 | require "rails/test_help"
4 |
5 | class ActiveSupport::TestCase
6 | # Run tests in parallel with specified workers
7 | parallelize(workers: :number_of_processors)
8 |
9 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
10 | fixtures :all
11 |
12 | # Add more helper methods to be used by all tests here...
13 | end
14 |
--------------------------------------------------------------------------------
/tmp/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/tmp/.keep
--------------------------------------------------------------------------------
/tmp/imports/watched/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/tmp/imports/watched/.keep
--------------------------------------------------------------------------------
/tmp/imports/watched/put-your-directory-here.txt:
--------------------------------------------------------------------------------
1 | The /tmp/imports/watched/USER@EMAIL.TLD directory is watched by Dawarich. Any files you put in this directory under a directory names with the email of the user you want to import the file for will be imported into the database.
2 |
3 | For example, if you want to import a file for the user with the email address "email@dawarich.app", you would place the file in the directory /tmp/imports/watched/email@dawarich.app. The file you place in this directory should be a GeoJSON or GPX file that contains the data you want to import. Dawarich automatically scans directories for new files every 60 minutes, on 0 minute of every hour, so you should see the file imported into the database within 1 hour of placing it in the directory.
4 |
5 |
--------------------------------------------------------------------------------
/tmp/pids/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/tmp/pids/.keep
--------------------------------------------------------------------------------
/tmp/storage/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/tmp/storage/.keep
--------------------------------------------------------------------------------
/vendor/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/vendor/.keep
--------------------------------------------------------------------------------
/vendor/javascript/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Freika/dawarich/52fd8982d8d079eaf7f340d19c2002e8098ad68d/vendor/javascript/.keep
--------------------------------------------------------------------------------