├── .gitattributes ├── .github └── dependabot.yml ├── .gitignore ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── app ├── assets │ ├── config │ │ └── manifest.js │ ├── images │ │ ├── .keep │ │ ├── logo.svg │ │ ├── logo_100.png │ │ ├── logo_1000.png │ │ ├── logo_200.png │ │ ├── logo_300.png │ │ ├── logo_50.png │ │ ├── logo_500.png │ │ └── logo_5000.png │ └── stylesheets │ │ └── application.css ├── channels │ └── application_cable │ │ ├── channel.rb │ │ └── connection.rb ├── controllers │ ├── announcement_controller.rb │ ├── application_controller.rb │ ├── concerns │ │ └── .keep │ ├── devapp_controller.rb │ ├── home_controller.rb │ ├── lobbying_controller.rb │ ├── meeting_controller.rb │ ├── parcels_controller.rb │ ├── team_controller.rb │ ├── traffic_cameras_controller.rb │ ├── transpo_controller.rb │ └── users │ │ └── omniauth_callbacks_controller.rb ├── helpers │ ├── announcement_helper.rb │ ├── application_helper.rb │ ├── devapp_helper.rb │ ├── home_helper.rb │ ├── meeting_helper.rb │ ├── parcels_helper.rb │ ├── team_helper.rb │ └── traffic_cameras_helper.rb ├── javascript │ ├── application.js │ └── controllers │ │ ├── application.js │ │ ├── hello_controller.js │ │ ├── index.js │ │ └── map_controller.js ├── jobs │ ├── application_job.rb │ ├── dev_app_scan_job.rb │ ├── lobbying_scan_job.rb │ ├── meeting_scan_job.rb │ ├── ping_job.rb │ ├── syndication_job.rb │ └── traffic_camera_scrape_job.rb ├── mailers │ └── application_mailer.rb ├── models │ ├── announcement.rb │ ├── application_record.rb │ ├── blue_sky.rb │ ├── committee.rb │ ├── concerns │ │ └── .keep │ ├── consultation.rb │ ├── consultation_scanner.rb │ ├── coordinates.rb │ ├── dev_app.rb │ ├── dev_app │ │ ├── address.rb │ │ ├── document.rb │ │ ├── entry.rb │ │ ├── scanner.rb │ │ └── status.rb │ ├── global_control.rb │ ├── lobbying_activity.rb │ ├── lobbying_undertaking.rb │ ├── mastedon_client.rb │ ├── meeting.rb │ ├── meeting_item.rb │ ├── meeting_item_document.rb │ ├── parcel.rb │ ├── parcel_scanner.rb │ ├── traffic_camera.rb │ ├── user.rb │ ├── v1 │ │ └── application_record.rb │ ├── zoning.rb │ └── zoning_scanner.rb └── views │ ├── announcement │ └── index.html.erb │ ├── devapp │ ├── _map_popup.html.erb │ ├── index.html.erb │ ├── map.html.erb │ └── show.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 │ │ ├── edit.html.erb │ │ └── new.html.erb │ ├── sessions │ │ └── new.html.erb │ ├── shared │ │ ├── _error_messages.html.erb │ │ └── _links.html.erb │ └── unlocks │ │ └── new.html.erb │ ├── home │ ├── index.html.erb │ └── index.rss.builder │ ├── layouts │ ├── application.html.erb │ ├── mailer.html.erb │ └── mailer.text.erb │ ├── lobbying │ ├── index.html.erb │ └── show.html.erb │ ├── meeting │ ├── index.html.erb │ └── show.html.erb │ ├── parcels │ └── show.html.erb │ ├── team │ └── index.html.erb │ ├── traffic_cameras │ ├── index.html.erb │ └── show.html.erb │ └── transpo │ └── show_stop.html.erb ├── bin ├── bundle ├── dbmysql ├── importmap ├── jobs ├── rails ├── rake ├── setup └── spring ├── config.ru ├── config ├── application.rb ├── boot.rb ├── cable.yml ├── credentials.yml.enc ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── importmap.rb ├── initializers │ ├── assets.rb │ ├── bugsnag.rb │ ├── content_security_policy.rb │ ├── devise.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ └── permissions_policy.rb ├── locales │ ├── devise.en.yml │ └── en.yml ├── puma.rb ├── queue.yml ├── recurring.yml ├── routes.rb └── storage.yml ├── db ├── cable_schema.rb ├── migrate │ ├── 20220122130113_create_active_storage_tables.active_storage.rb │ ├── 20220207012208_create_dev_app_entries.rb │ ├── 20220208170430_create_dev_app_addresses.rb │ ├── 20220209221445_create_dev_app_documents.rb │ ├── 20220211193125_add_desc_to_dev_app_entry.rb │ ├── 20220211193615_create_dev_app_statuses.rb │ ├── 20220212131300_create_announcements.rb │ ├── 20220221183941_create_parcels.rb │ ├── 20220402214403_dev_app_entry_description_wider.rb │ ├── 20220403190017_create_global_controls.rb │ ├── 20220409210514_add_state_to_dev_app_documents.rb │ ├── 20220410023931_add_planner_to_dev_app_entry.rb │ ├── 20220416031244_create_committees.rb │ ├── 20220416031533_create_meetings.rb │ ├── 20220416031611_create_meeting_items.rb │ ├── 20220416042402_change_meeting_start_time.rb │ ├── 20220416183233_add_meeting_to_meeting_items.rb │ ├── 20220425003142_create_lobbying_undertakings.rb │ ├── 20220425004321_create_lobbying_activities.rb │ ├── 20220502212849_create_elections.rb │ ├── 20220502213209_create_candidates.rb │ ├── 20220522030307_add_geometry_to_parcels.rb │ ├── 20220522031853_change_geometry_json_to_medium_text.rb │ ├── 20220527125853_add_unique_index_to_parcels.rb │ ├── 20220805155127_add_reference_guid_to_meetings.rb │ ├── 20220821234844_add_withdrew_to_candidates.rb │ ├── 20220915011507_devise_create_users.rb │ ├── 20221018215417_add_provider_to_users.rb │ ├── 20221018215842_add_name_to_users.rb │ ├── 20221018220043_add_username_to_users.rb │ ├── 20221018220420_change_users_table.rb │ ├── 20221019023256_create_consultations.rb │ ├── 20221107140208_create_service_requests.rb │ ├── 20221108022353_change_service_requests_times_to_dates.rb │ ├── 20221115154752_create_zonings.rb │ ├── 20230206010757_create_campaign_returns.rb │ ├── 20230206010904_create_campaign_return_pages.rb │ ├── 20230227025209_add_rotation_to_campaign_return_pages.rb │ ├── 20230227133754_drop_campaign_donation.rb │ ├── 20230227134154_create_campaign_donations.rb │ ├── 20230403222136_add_url_to_campaign_returns.rb │ ├── 20240107220707_create_meeting_item_documents.rb │ ├── 20240108032419_add_content_to_meeting_items.rb │ ├── 20240123032506_alter_title_on_meeting_items.rb │ ├── 20240128024632_add_snapshot_date_to_parcels.rb │ ├── 20240128025544_drop_object_id_index_from_parcels.rb │ ├── 20240128030031_add_scan_object_id_index_to_parcels.rb │ ├── 20240201195110_add_snapshot_date_to_zonings.rb │ ├── 20241006165929_create_traffic_cameras.rb │ ├── 20250222190338_create_solid_cable_tables.rb │ ├── 20250222200721_create_solid_queue_tables.rb │ └── 20250304022406_add_client_details_to_lobbying_undertaking.rb ├── schema.rb ├── seeds.rb └── v1_schema.rb ├── docker ├── Dockerfile.base ├── Dockerfile.dev ├── Dockerfile.prod ├── dev-build.sh ├── dev-exec.sh ├── dev-run.sh ├── pdev-build.sh ├── pdev-run.sh ├── prod-build.sh ├── prod-deploy.sh ├── prod-exec.sh ├── prod-stop.sh └── prod-web.sh ├── fixtures ├── four_pages.pdf └── vcr_cassettes │ ├── AnnouncementTest_test_reference_context_for_LobbyingUndertaking_format.yml │ ├── BlueSkyTest_test__create_post_posts_successfully.yml │ ├── ConsultationScannerTest_test_big_integration_test_dont_judge_me.yml │ ├── DevApp_ScannerTest_test_addresses_get_saved.yml │ ├── DevApp_ScannerTest_test_ensure_app_id_collisions_are_handled.yml │ ├── DevApp_ScannerTest_test_file_descriptions_are_normalized.yml │ ├── DevApp_ScannerTest_test_file_urls_are_encoded_properly.yml │ ├── DevApp_ScannerTest_test_files_get_saved.yml │ ├── DevApp_ScannerTest_test_issue_102_regression_fix.yml │ ├── DevApp_ScannerTest_test_issue_102_regression_fix_02.yml │ ├── DevApp_ScannerTest_test_new_devapp_is_announced_as_new.yml │ ├── DevApp_ScannerTest_test_planner_contact_data_is_collected.yml │ ├── DevApp_ScannerTest_test_scanning_an_application_generates_an_entry__2nd_can_updates_previous_entry.yml │ ├── DevApp_ScannerTest_test_status_gets_saved.yml │ ├── LobbyingScanJobTest_test_all_details_of_a_lobbying_undertaking_are_captured.yml │ ├── LobbyingScanJobTest_test_existing_lobbying_without_an_announcement_dont_get_announced_on_re-scan.yml │ ├── LobbyingScanJobTest_test_fix_failure_occurring_for_date_2023-01-30.yml │ ├── LobbyingScanJobTest_test_lobbying_activity_dates_are_parsed_and_saved_correctly.yml │ ├── LobbyingScanJobTest_test_new_lobbying_activities_are_announced.yml │ ├── LobbyingScanJobTest_test_specific_dates_can_be_scraped.yml │ ├── MeetingScanJobTest_test_Agriculture_and_Rural_Affairs_Committee_4f806962-c059-4605-b48c-751daee8bd85_can_be_scanned.yml │ ├── MeetingScanJobTest_test_City_Council_59a74d3a-4563-4269-9196-ab3bea684571_can_be_scanned.yml │ ├── MeetingScanJobTest_test_Committee_of_Adjustment_-_Panel_1_d0f46ee8-dbd2-4f80-99aa-e8d7fa5a0742_can_be_scanned.yml │ ├── MeetingScanJobTest_test_Committee_of_Adjustment_-_Panel_2_e5affc34-2148-4958-a978-99647b66492d_can_be_scanned.yml │ ├── MeetingScanJobTest_test_Committee_of_Adjustment_-_Panel_3_2dd97c8d-fdc0-4ecb-833e-6d5c8489d552_can_be_scanned.yml │ ├── MeetingScanJobTest_test_Planning_Committee_128fff38-faa9-4b07-a8cc-e13e88688f9d_can_be_scanned.yml │ ├── MeetingScanJobTest_test_Police_Services_Board_Human_Resources_Committee_ce1a3efd-4f33-4838-8aae-76f7123aed8c_can_be_scanned.yml │ ├── MeetingScanJobTest_test_in-camera_items_do_not_have_AgendaItemXXX_class_names_as_they_are_hidden.yml │ ├── MeetingScanJobTest_test_issue_112__item_documents_are_404ing_after_items_are_edited_at_source.yml │ ├── MeetingScanJobTest_test_issue_96__regression.yml │ ├── MeetingScanJobTest_test_meeting_items_and_docs_are_parsed__saved__not_duplicated.yml │ ├── MeetingScanJobTest_test_no_argument_job_inhales_the_meeting_index_and_enqueues_subsequent_jobs.yml │ ├── MeetingScanJobTest_test_previous_agenda_formats_are_also_scanned_for_items_and_docs.yml │ ├── MeetingScanJobTest_test_regression_issue_93__title_was_too_long_for_schema.yml │ ├── ParcelTest_test__objects_after_returns_objects_after_the_given_one.yml │ ├── ParcelTest_test__perform_loads_new_entries_starting_with_largest_objectid.yml │ ├── ParcelTest_test__perform_uses_first_day_of_month_as_snapshot_date_and_pulls_a_full_clone_each_month.yml │ ├── TrafficCameraTest_test__captures_inserts_a_row_in_the_SQLite_archive.yml │ ├── TrafficCameraTest_test__captures_returns_the_correct_captures.yml │ ├── TrafficCameraTest_test_cameras_are_not_duplicated_when_scraped_again.yml │ ├── TrafficCameraTest_test_cameras_are_scraped_correctly.yml │ ├── TrafficCameraTest_test_traffic_camera_images_are_captured_correctly.yml │ ├── ZoningScannerTest_test__perform_starts_at_0_and_moves_forward_in_steps_of_1000.yml │ └── ZoningScannerTest_test__perform_uses_first_day_of_month_as_snapshot_date_and_pulls_a_full_clone_each_month.yml ├── lib ├── assets │ └── .keep └── tasks │ ├── .keep │ └── ottwatch.rake ├── log └── .keep ├── public ├── 404.html ├── 422.html ├── 500.html ├── apple-touch-icon-precomposed.png ├── apple-touch-icon.png ├── favicon.ico └── robots.txt ├── storage └── .keep ├── test ├── application_system_test_case.rb ├── channels │ └── application_cable │ │ └── connection_test.rb ├── controllers │ ├── .keep │ ├── announcement_controller_test.rb │ ├── devapp_controller_test.rb │ ├── home_controller_test.rb │ ├── lobbying_controller_test.rb │ ├── meeting_controller_test.rb │ ├── parcels_controller_test.rb │ ├── team_controller_test.rb │ ├── traffic_cameras_controller_test.rb │ └── transpo_controller_test.rb ├── factories │ ├── campaign_donations.rb │ └── meeting_item_documents.rb ├── fixtures │ ├── committees.yml │ ├── dev_app │ │ ├── addresses.yml │ │ ├── documents.yml │ │ ├── entries.yml │ │ └── statuses.yml │ ├── files │ │ ├── .keep │ │ └── dev_apps.xlsx │ └── global_controls.yml ├── helpers │ └── .keep ├── integration │ └── .keep ├── jobs │ ├── dev_app_scan_job_test.rb │ ├── lobbying_scan_job_test.rb │ ├── meeting_scan_job_test.rb │ ├── ping_job_test.rb │ ├── syndication_job_test.rb │ └── traffic_camera_scrape_job_test.rb ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── announcement_test.rb │ ├── blue_sky_test.rb │ ├── committee_test.rb │ ├── consultation_scanner_test.rb │ ├── consultation_test.rb │ ├── coordinates_test.rb │ ├── dev_app │ │ ├── address_test.rb │ │ ├── document_test.rb │ │ ├── entry_test.rb │ │ ├── scanner_test.rb │ │ └── status_test.rb │ ├── global_control_test.rb │ ├── lobbying_activity_test.rb │ ├── lobbying_undertaking_test.rb │ ├── meeting_item_document_test.rb │ ├── meeting_item_test.rb │ ├── meeting_test.rb │ ├── parcel_test.rb │ ├── traffic_camera_test.rb │ ├── user_test.rb │ ├── zoning_scanner_test.rb │ └── zoning_test.rb ├── system │ └── .keep └── test_helper.rb ├── tmp ├── .keep ├── pids │ └── .keep └── storage │ └── .keep └── vendor ├── .keep └── javascript └── .keep /.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/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" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore the default SQLite database. 11 | /db/*.sqlite3 12 | /db/*.sqlite3-* 13 | 14 | # Ignore all logfiles and tempfiles. 15 | /log/* 16 | /tmp/* 17 | !/log/.keep 18 | !/tmp/.keep 19 | 20 | # Ignore pidfiles, but keep the directory. 21 | tmp/pids/* 22 | !tmp/pids/ 23 | !tmp/pids/.keep 24 | 25 | # Ignore uploaded files in development. 26 | /storage/* 27 | !/storage/.keep 28 | tmp/storage/* 29 | !tmp/storage/ 30 | !tmp/storage/.keep 31 | 32 | /public/assets 33 | 34 | # Ignore master key for decrypting credentials and more. 35 | /config/master.key 36 | 37 | /vendor/bundle 38 | 39 | db/ottwatch_v1_snapshot.sql 40 | .DS_Store 41 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-3.0.2 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 3 | 4 | # ruby "3.2.3" 5 | 6 | # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" 7 | gem "rails", "~> 8.0" 8 | 9 | # The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] 10 | gem "sprockets-rails" 11 | 12 | # Use sqlite3 as the database for Active Record 13 | gem 'sqlite3', '~> 2.0', '>= 2.0.2' 14 | 15 | gem "mysql2" 16 | 17 | # Use the Puma web server [https://github.com/puma/puma] 18 | gem "puma", "~> 6.6" 19 | 20 | # https://www.reddit.com/r/rails/comments/19de7ju/rails_official_guide_installation_not_working/ 21 | gem "psych", "~> 4" 22 | 23 | # Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] 24 | gem "importmap-rails" 25 | 26 | # Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] 27 | gem "turbo-rails" 28 | 29 | # Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] 30 | gem "stimulus-rails" 31 | 32 | # Build JSON APIs with ease [https://github.com/rails/jbuilder] 33 | gem "jbuilder" 34 | 35 | # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] 36 | # gem "bcrypt", "~> 3.1.7" 37 | 38 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 39 | # gem "tzinfo-data", platforms: %i[ mingw mswin x64_mingw jruby ] 40 | 41 | # Reduces boot times through caching; required in config/boot.rb 42 | gem "bootsnap", require: false 43 | 44 | # Use Sass to process CSS 45 | # gem "sassc-rails" 46 | 47 | # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] 48 | gem "image_processing" 49 | gem "mini_magick", "~> 4.13" 50 | 51 | gem "google-cloud-storage", require: false 52 | gem "xsv" 53 | gem "twitter" 54 | gem 'tzinfo-data' 55 | gem 'devise' 56 | # gem 'omniauth' 57 | gem "omniauth", "~> 2.1.2" # Can not move to 2.0 because of devise - https://github.com/heartcombo/devise/pull/5327 58 | gem 'omniauth-github' 59 | gem 'omniauth-google-oauth2' 60 | gem "rmagick" 61 | 62 | group :development, :test do 63 | # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem 64 | # gem "httplog" 65 | gem "debug", platforms: %i[ mri mingw x64_mingw ] 66 | gem "pry" 67 | gem "factory_bot_rails" 68 | gem 'bullet' 69 | end 70 | 71 | group :development do 72 | # Use console on exceptions pages [https://github.com/rails/web-console] 73 | gem "web-console" 74 | 75 | # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler] 76 | # gem "rack-mini-profiler" 77 | 78 | # Speed up commands on slow machines / big apps [https://github.com/rails/spring] 79 | gem "spring" 80 | gem 'rubocop', require: false 81 | end 82 | 83 | group :test do 84 | # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] 85 | gem "capybara" 86 | gem "selenium-webdriver" 87 | gem "webdrivers" 88 | gem "mocha" 89 | gem "minitest-focus" 90 | gem "webmock" 91 | gem "vcr" 92 | end 93 | 94 | gem "bugsnag", "~> 6.25" 95 | 96 | gem "solid_cable", "~> 3.0" 97 | 98 | gem "solid_queue", "~> 1.1" 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | ## OttWatch 4 | 5 | OttWatch is a tool for monitoring and analyzing municipal activities in Ottawa, providing citizens with transparent access to local government operations. 6 | 7 | ## Exporting ottwatch.v1 database 8 | 9 | ``` 10 | MYUSER="root" 11 | MYPASS="XXX" 12 | MYSQLDUMP=" mysqldump --complete-insert --extended-insert=false -u $MYUSER --password=$MYPASS " 13 | 14 | $MYSQLDUMP ottwatch \ 15 | election \ 16 | candidate \ 17 | candidate_return \ 18 | candidate_donation \ 19 | > ottwatch_v1_snapshot.sql 20 | ``` 21 | 22 | ## Getting Started 23 | 24 | Follow these steps to set up and run OttWatch locally on your machine. 25 | 26 | ### Prerequisites 27 | 28 | - Git 29 | - GitHub account 30 | - Docker (or Podman for alternative container runtime) 31 | 32 | ### Installation 33 | 34 | 1. Fork the repository: 35 | - Visit the OttWatch GitHub repository: https://github.com/original-owner/ottwatch 36 | - Click the "Fork" button in the top-right corner to create your own copy of the repository 37 | 38 | 2. Clone your forked repository: 39 | ``` 40 | git clone https://github.com/your-username/ottwatch.git 41 | cd ottwatch 42 | ``` 43 | 44 | 3. Build the Docker image: 45 | ``` 46 | cd docker 47 | ./dev-build.sh # Use ./pdev-build.sh if using Podman 48 | ``` 49 | 50 | 4. Run the Docker container: 51 | ``` 52 | ./dev-run.sh # Use ./pdev-run.sh if using Podman 53 | ``` 54 | 55 | 5. Inside the container, start MySQL and set up the database: 56 | ``` 57 | /etc/init.d/mysql start 58 | cd ottwatch 59 | bin/rails db:setup 60 | ``` 61 | 62 | 6. Start the Rails server: 63 | ``` 64 | rails s 65 | ``` 66 | 67 | 7. Access the application: 68 | Open your web browser and navigate to `http://localhost:33000` 69 | 70 | ## Development 71 | 72 | For subsequent runs, you only need to execute `./dev-run.sh` (or `./pdev-run.sh`) from the `docker` directory to start the container and access the development environment. 73 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative "config/application" 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../stylesheets .css 3 | //= link_tree ../../javascript .js 4 | //= link_tree ../../../vendor/javascript .js 5 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinodotnet/ottwatch/2e337b8583bb932234052e6c6a3a4aac516bcd5a/app/assets/images/.keep -------------------------------------------------------------------------------- /app/assets/images/logo_100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinodotnet/ottwatch/2e337b8583bb932234052e6c6a3a4aac516bcd5a/app/assets/images/logo_100.png -------------------------------------------------------------------------------- /app/assets/images/logo_1000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinodotnet/ottwatch/2e337b8583bb932234052e6c6a3a4aac516bcd5a/app/assets/images/logo_1000.png -------------------------------------------------------------------------------- /app/assets/images/logo_200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinodotnet/ottwatch/2e337b8583bb932234052e6c6a3a4aac516bcd5a/app/assets/images/logo_200.png -------------------------------------------------------------------------------- /app/assets/images/logo_300.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinodotnet/ottwatch/2e337b8583bb932234052e6c6a3a4aac516bcd5a/app/assets/images/logo_300.png -------------------------------------------------------------------------------- /app/assets/images/logo_50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinodotnet/ottwatch/2e337b8583bb932234052e6c6a3a4aac516bcd5a/app/assets/images/logo_50.png -------------------------------------------------------------------------------- /app/assets/images/logo_500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinodotnet/ottwatch/2e337b8583bb932234052e6c6a3a4aac516bcd5a/app/assets/images/logo_500.png -------------------------------------------------------------------------------- /app/assets/images/logo_5000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinodotnet/ottwatch/2e337b8583bb932234052e6c6a3a4aac516bcd5a/app/assets/images/logo_5000.png -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's 6 | * vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS 10 | * files in this directory. Styles in this file should be added after the last require_* statement. 11 | * It is generally better to create a new file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | 17 | .app-notice { 18 | margin-top: 5px; 19 | margin-bottom: 5px; 20 | padding: 5px; 21 | font-size: 150%; 22 | font-weight: bold; 23 | border: solid 3px #f5a442; 24 | background: #f0f0f0; 25 | } 26 | 27 | .app-alert { 28 | margin-top: 5px; 29 | margin-bottom: 5px; 30 | padding: 5px; 31 | font-size: 150%; 32 | font-weight: bold; 33 | background: #f0f0f0; 34 | border: solid 3px #f54842; 35 | } 36 | 37 | a { 38 | text-decoration: none !important; 39 | } 40 | a:hover { 41 | text-decoration: underline !important; 42 | } 43 | 44 | .new_announcement_badge { 45 | color: #7aa126; 46 | } 47 | 48 | .owrows { 49 | /* border-top: solid 1px #c0c0c0; 50 | border-bottom: solid 1px #c0c0c0; 51 | */ 52 | } 53 | .owrows .row:nth-child(odd) { 54 | background: #f0f0f0; 55 | padding-top: 5px; 56 | padding-bottom: 5px; 57 | } 58 | .owrows .row:nth-child(even) { 59 | padding-top: 5px; 60 | padding-bottom: 5px; 61 | } 62 | .ow_footer { 63 | margin-top: 25px; 64 | padding: 10px; 65 | background: #f0f0f0; 66 | /* border: solid 1px #c0c0c0; */ 67 | font-size: 90%; 68 | font-style: italic; 69 | } 70 | 71 | .camera-grid { 72 | display: flex; 73 | flex-wrap: wrap; 74 | } 75 | 76 | .camera-item { 77 | width: 150px; 78 | height: 150px; 79 | } 80 | 81 | .camera-item img { 82 | width: 100%; 83 | height: 100%; 84 | object-fit: cover; 85 | } -------------------------------------------------------------------------------- /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 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/announcement_controller.rb: -------------------------------------------------------------------------------- 1 | class AnnouncementController < ApplicationController 2 | def index 3 | relation = Announcement.all 4 | relation = relation.where(reference_type: params[:reference_type]) if params[:reference_type] 5 | relation = relation.where('id < ?', params[:before_id]) if params[:before_id] 6 | relation = relation.limit(params[:limit] || 50).includes(:reference) 7 | @announcements = relation.order(id: :desc).to_a 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | end 3 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinodotnet/ottwatch/2e337b8583bb932234052e6c6a3a4aac516bcd5a/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/controllers/devapp_controller.rb: -------------------------------------------------------------------------------- 1 | class DevappController < ApplicationController 2 | def index 3 | limit = params["limit"] || 10 4 | relation = if params["before_id"] 5 | DevApp::Entry.where("id < ?", params["before_id"]) 6 | else 7 | DevApp::Entry.all 8 | end 9 | 10 | @devapps = relation.includes(:addresses).order(updated_at: :desc).limit(limit) 11 | end 12 | 13 | def show 14 | @entry = DevApp::Entry.where(app_number: params[:app_number]).includes(:statuses, :addresses, :documents).first 15 | return render plain: "404 Not Found", status: 404 unless @entry 16 | 17 | @coordinates = @entry.addresses.first&.coordinates 18 | end 19 | 20 | def map 21 | ottawa_city_hall = Coordinates.new(45.420906, -75.689374) 22 | @initial_lat = ottawa_city_hall.lat 23 | @initial_lon = ottawa_city_hall.lon 24 | 25 | @statuses = DevApp::Status.distinct.pluck(:status) - ["404_missing_data"] 26 | @app_types = DevApp::Entry.distinct.pluck(:app_type).sort 27 | end 28 | 29 | def map_data 30 | geojson = { 31 | type: "FeatureCollection", 32 | # TODO: group the data by unique address and update the popup to show the different apps at the address. 33 | # Unsure at the moment whether the map is move valuable showing distinct applications (pin to first address) or 34 | # distinct list of addresses and possible displaying the same app multiple times. 35 | features: DevApp::Entry.includes(:addresses, :statuses).filter_map do |app| 36 | next unless (coordinates = app.addresses.first&.coordinates) 37 | 38 | { 39 | type: "Feature", 40 | geometry: { 41 | type: "Point", 42 | coordinates: [coordinates.lon, coordinates.lat], 43 | }, 44 | properties: { 45 | id: app.id, 46 | app_number: app.app_number, 47 | app_type: app.app_type, 48 | status: app.current_status.status, 49 | description: app.desc&.truncate(140, separator: /\s/), 50 | url: url_for(controller: 'devapp', action: 'show', app_number: app.app_number) 51 | } 52 | } 53 | end 54 | } 55 | 56 | render json: geojson 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /app/controllers/home_controller.rb: -------------------------------------------------------------------------------- 1 | class HomeController < ApplicationController 2 | def index 3 | # @announcements = Announcement.all.includes(reference: :addresses).order(id: :desc).limit(50) 4 | @announcements = Announcement.all.includes([:reference]).order(id: :desc).limit(10) 5 | 6 | @meetings = Meeting.includes(:committee) 7 | .where('start_time > ?', Time.now.beginning_of_day) 8 | .order(:start_time) 9 | 10 | respond_to do |format| 11 | format.html 12 | format.rss { render :layout => false } 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/controllers/lobbying_controller.rb: -------------------------------------------------------------------------------- 1 | class LobbyingController < ApplicationController 2 | def index 3 | relation = LobbyingUndertaking.all 4 | if params["before_id"] 5 | relation = relation.where("id < ?", params["before_id"]) 6 | end 7 | if params["before_created_at"] 8 | relation = relation.where("created_at <= ?", params["before_created_at"]) 9 | end 10 | @undertakings = relation.order(created_at: :desc, id: :desc).limit(100) 11 | agg_relation = LobbyingActivity.group(:lobbying_undertaking_id).where(lobbying_undertaking_id: @undertakings.map(&:id)) 12 | @counts = agg_relation.count 13 | @latest = agg_relation.maximum(:activity_date) 14 | @first = agg_relation.minimum(:activity_date) 15 | end 16 | 17 | def show 18 | @undertaking = LobbyingUndertaking.find(params[:id]) 19 | raise ActionController::RoutingError.new('Lobbying Entry Not Found') unless @undertaking 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/controllers/meeting_controller.rb: -------------------------------------------------------------------------------- 1 | class MeetingController < ApplicationController 2 | def index 3 | @upcoming = Meeting.includes(:committee) 4 | .where('start_time > ?', Time.now.beginning_of_day + 1.day) 5 | .order(:start_time) 6 | 7 | @today = Meeting.includes(:committee) 8 | .where('start_time > ?', Time.now.beginning_of_day) 9 | .where('start_time < ?', Time.now.beginning_of_day + 1.day) 10 | .order(:start_time) 11 | 12 | relation = if params["before_id"] 13 | m = Meeting.find(params["before_id"]) 14 | Meeting.where("start_time < ? and id < ?", m.start_time, params["before_id"]) 15 | else 16 | Meeting.where('start_time < ?', Time.now.beginning_of_day) 17 | end 18 | @previous = relation.includes(:committee).order(start_time: :desc, id: :desc).limit(50) 19 | end 20 | 21 | def show 22 | @meeting = Meeting.where(reference_id: params[:reference_id]) 23 | .or(Meeting.where(reference_guid: params[:reference_id])) 24 | .includes(items: :documents) 25 | .first 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/controllers/parcels_controller.rb: -------------------------------------------------------------------------------- 1 | class ParcelsController < ApplicationController 2 | before_action :authenticate_user! 3 | 4 | def show 5 | @parcel = Parcel.find(params[:id]) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/controllers/team_controller.rb: -------------------------------------------------------------------------------- 1 | class TeamController < ApplicationController 2 | before_action :authenticate_user! 3 | 4 | def index 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/controllers/traffic_cameras_controller.rb: -------------------------------------------------------------------------------- 1 | class TrafficCamerasController < ApplicationController 2 | def index 3 | @traffic_cameras = TrafficCamera.all.order(:id) 4 | end 5 | 6 | def show 7 | @traffic_camera = TrafficCamera.find(params[:id]) 8 | end 9 | 10 | def capture 11 | # params: { id: '1010' } 12 | # params: { time_ms: '1717234234' } 13 | @traffic_camera = TrafficCamera.find(params[:id]) 14 | capture = @traffic_camera.captures.detect{|c| c[:time_ms] == params[:time_ms].to_i} 15 | return unless capture 16 | response.headers['Cache-Control'] = 'public, max-age=86400' 17 | send_data(File.read(capture[:file]), type: 'image/jpeg', disposition: 'inline', filename: "cam_#{params[:id]}_#{params[:time_ms]}.jpg") 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/controllers/transpo_controller.rb: -------------------------------------------------------------------------------- 1 | class TranspoController < ApplicationController 2 | def self.get_next_trips_for_stop_all_routes(stop_no) 3 | app_id = ENV["OCTRANSPO_APP_ID"] 4 | api_key = ENV["OCTRANSPO_APP_KEY"] 5 | 6 | url = "https://api.octranspo1.com/v2.0/GetNextTripsForStopAllRoutes?appID=#{app_id}&apiKey=#{api_key}&stopNo=#{stop_no}&format=JSON" 7 | data = Net::HTTP.get(URI(url)) 8 | Rails.logger.info(data) 9 | data 10 | end 11 | 12 | def self.stop_trips(stop_no) 13 | data = get_next_trips_for_stop_all_routes(stop_no) 14 | 15 | begin 16 | data = JSON.parse(data)["GetRouteSummaryForStopResult"] 17 | rescue => e 18 | return :api_error 19 | end 20 | 21 | 22 | routes = data["Routes"]["Route"] 23 | routes = [routes] if routes.is_a?(Hash) 24 | routes = routes.map do |r| 25 | trips = if r["Trips"].is_a?(Array) 26 | r["Trips"] 27 | elsif r["Trips"].is_a?(Hash) 28 | if r["Trips"]["Trip"] 29 | r["Trips"]["Trip"] 30 | else 31 | [r["Trips"]] 32 | end 33 | end 34 | 35 | trips = trips.map do |t| 36 | age = (t["AdjustmentAge"].to_f * 60).round 37 | { 38 | # lat: t["Latitude"], 39 | # lon: t["Longitude"], 40 | # speed: t["GPSSpeed"], 41 | dest: t["TripDestination"], 42 | in: t["AdjustedScheduleTime"], 43 | age: age, 44 | per: (age < 0 ? :sched : :gps) 45 | } 46 | end 47 | { 48 | no: r["RouteNo"], 49 | heading: r["RouteHeading"], 50 | trips: trips 51 | } 52 | end 53 | { 54 | no: data["StopNo"], 55 | desc: data["StopDescription"], 56 | routes: routes 57 | } 58 | end 59 | 60 | def show_stop 61 | @stop_data = self.class.stop_trips(params[:stop_no]) if params[:stop_no] 62 | return if @stop_data == :api_error 63 | if params[:stop_routes] && params[:stop_routes].size > 0 64 | keep_routes = params[:stop_routes].split(" ") 65 | @stop_data[:routes] = @stop_data[:routes].select{|r| keep_routes.include?(r[:no])} 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /app/controllers/users/omniauth_callbacks_controller.rb: -------------------------------------------------------------------------------- 1 | class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController 2 | def google_oauth2 3 | generic_handler("google") 4 | end 5 | 6 | def github 7 | generic_handler("github") 8 | end 9 | 10 | def generic_handler(kind) 11 | Rails.logger.info("omniauth.auth: #{request.env["omniauth.auth"]}") 12 | @user = User.from_omniauth(request.env["omniauth.auth"]) 13 | Rails.logger.info( 14 | msg: "user", 15 | user_id: @user.id, 16 | user_email: @user.email, 17 | provider: @user.provider, 18 | uuid: @user.uid, 19 | persisted: @user.persisted?, 20 | errors: @user.errors.full_messages 21 | ) 22 | 23 | if @user.errors.any? 24 | flash.alert = @user.errors.full_messages 25 | redirect_to root_path 26 | else 27 | sign_in_and_redirect @user, event: :authentication #this will throw if @user is not activated 28 | set_flash_message(:notice, :success, kind: kind) if is_navigational_format? 29 | end 30 | end 31 | 32 | def failure 33 | redirect_to root_path 34 | end 35 | end -------------------------------------------------------------------------------- /app/helpers/announcement_helper.rb: -------------------------------------------------------------------------------- 1 | module AnnouncementHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/devapp_helper.rb: -------------------------------------------------------------------------------- 1 | module DevappHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/home_helper.rb: -------------------------------------------------------------------------------- 1 | module HomeHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/meeting_helper.rb: -------------------------------------------------------------------------------- 1 | module MeetingHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/parcels_helper.rb: -------------------------------------------------------------------------------- 1 | module ParcelsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/team_helper.rb: -------------------------------------------------------------------------------- 1 | module TeamHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/traffic_cameras_helper.rb: -------------------------------------------------------------------------------- 1 | module TrafficCamerasHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/javascript/application.js: -------------------------------------------------------------------------------- 1 | // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails 2 | import "@hotwired/turbo-rails" 3 | import "controllers" 4 | -------------------------------------------------------------------------------- /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/hello_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | connect() { 5 | this.element.textContent = "Hello World!" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/javascript/controllers/index.js: -------------------------------------------------------------------------------- 1 | // Import and register all your controllers from the importmap under controllers/* 2 | 3 | import { application } from "controllers/application" 4 | 5 | // Eager load all controllers defined in the import map under controllers/**/*_controller 6 | import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" 7 | eagerLoadControllersFrom("controllers", application) 8 | 9 | // Lazy load controllers as they appear in the DOM (remember not to preload controllers in import map!) 10 | // import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading" 11 | // lazyLoadControllersFrom("controllers", application) 12 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | # Automatically retry jobs that encountered a deadlock 3 | # retry_on ActiveRecord::Deadlocked 4 | 5 | # Most jobs are safe to ignore if the underlying records are no longer available 6 | # discard_on ActiveJob::DeserializationError 7 | end 8 | -------------------------------------------------------------------------------- /app/jobs/dev_app_scan_job.rb: -------------------------------------------------------------------------------- 1 | class DevAppScanJob < ApplicationJob 2 | queue_as :default 3 | 4 | def perform(app_number: nil) 5 | if app_number 6 | DevApp::Scanner.scan_application(app_number) 7 | else 8 | enqueued = Set.new 9 | DevApp::Scanner.latest.each do |d| 10 | next if enqueued.include?(d[:app_number]) 11 | DevAppScanJob.perform_later(app_number: d[:app_number]) 12 | enqueued << d[:app_number] 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/jobs/ping_job.rb: -------------------------------------------------------------------------------- 1 | class PingJob < ApplicationJob 2 | queue_as :default 3 | 4 | # https://edgeguides.rubyonrails.org/active_job_basics.html#enqueue-the-job 5 | # # Enqueue a job to be performed as soon as the queuing system is 6 | # # free. 7 | # GuestsCleanupJob.perform_later guest 8 | # Copy 9 | # # Enqueue a job to be performed tomorrow at noon. 10 | # GuestsCleanupJob.set(wait_until: Date.tomorrow.noon).perform_later(guest) 11 | # Copy 12 | # # Enqueue a job to be performed 1 week from now. 13 | # GuestsCleanupJob.set(wait: 1.week).perform_later(guest) 14 | # Copy 15 | # # `perform_now` and `perform_later` will call `perform` under the hood so 16 | # # you can pass as many arguments as defined in the latter. 17 | # GuestsCleanupJob.perform_later(guest1, guest2, filter: 'some_filter') 18 | 19 | def perform(*args) 20 | Rails.logger.info("#" * 50) 21 | Rails.logger.info("ping: #{job_id}") 22 | Rails.logger.info("#" * 50) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/jobs/syndication_job.rb: -------------------------------------------------------------------------------- 1 | class SyndicationJob < ApplicationJob 2 | queue_as :default 3 | 4 | GLOBAL_CONFIG_KEY = "syndication_job_last_id" 5 | 6 | def syndicate(a) 7 | msg = a.message 8 | msg << " (#{a.reference_context})" if a.reference_context 9 | msg << " #{a.reference_link}" 10 | BlueSky.new.skeet(msg) 11 | end 12 | 13 | def perform 14 | announcements.each do |a| 15 | GlobalControl.set(GLOBAL_CONFIG_KEY, a.id) 16 | syndicate(a) 17 | end 18 | end 19 | 20 | private 21 | 22 | def announcements 23 | last_id = GlobalControl.get(GLOBAL_CONFIG_KEY) || Announcement.last.id 24 | Announcement.where('id > ?', last_id).order(:id).limit(5) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/jobs/traffic_camera_scrape_job.rb: -------------------------------------------------------------------------------- 1 | class TrafficCameraScrapeJob < ApplicationJob 2 | queue_as :default 3 | 4 | def perform 5 | TrafficCamera.scrape_all 6 | TrafficCamera.all.each do |camera| 7 | camera.capture_image 8 | end 9 | end 10 | end 11 | 12 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: "from@example.com" 3 | layout "mailer" 4 | end 5 | -------------------------------------------------------------------------------- /app/models/announcement.rb: -------------------------------------------------------------------------------- 1 | class Announcement < ApplicationRecord 2 | belongs_to :reference, polymorphic: true 3 | 4 | # TODO: this should be in an ERB helper? 5 | def font_awesome_class 6 | case reference_type 7 | when "Consultation" 8 | "fa-solid fa-square-poll-horizontal" 9 | when "DevApp::Entry" 10 | "fa-solid fa-building" 11 | when "LobbyingUndertaking" 12 | "fa-solid fa-handshake" 13 | when "Meeting" 14 | "fa-solid fa-calendar" 15 | else 16 | "fa-solid fa-question" 17 | end 18 | end 19 | 20 | def reference_context 21 | if reference.is_a?(Consultation) 22 | return reference.title 23 | end 24 | if reference.is_a?(DevApp::Entry) 25 | if addr = reference.addresses.first 26 | parts = [addr.road_number, addr.road_name, addr.road_type, addr.direction].reject{|c| c == ""} 27 | return nil if parts.count < 2 28 | return parts.join(" ") 29 | end 30 | end 31 | return "#{reference.start_time.in_time_zone("America/New_York").strftime("%b %d %H:%M")}" if reference.is_a?(Meeting) 32 | if reference.is_a?(LobbyingUndertaking) 33 | issue = reference.issue || "" 34 | "#{reference.lobbyist_name} (#{reference.lobbyist_position}): #{issue.split(" ").first(10).join(" ")} ..." 35 | end 36 | end 37 | 38 | def reference_link 39 | return reference.full_href if reference.is_a?(Consultation) 40 | 41 | # TODO: this should move to a helper that can be used in the UI as well 42 | url = if Rails.env.production? 43 | "https://ottwatch.ca" 44 | else 45 | "http://localhost:33000" 46 | end 47 | 48 | return "#{url}/devapp/#{reference.app_number}" if reference.is_a?(DevApp::Entry) 49 | if reference.is_a?(Meeting) 50 | return "#{url}/meeting/#{reference.reference_id}" if reference.reference_id 51 | return "#{url}/meeting/#{reference.reference_guid}" if reference.reference_guid 52 | end 53 | return "#{url}/lobbying/#{reference.id}" if reference.is_a?(LobbyingUndertaking) 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | primary_abstract_class 3 | 4 | connects_to database: { writing: :primary, reading: :primary } 5 | end -------------------------------------------------------------------------------- /app/models/blue_sky.rb: -------------------------------------------------------------------------------- 1 | # 2 | # With thanks to https://t27duck.com/posts/17-a-bluesky-at-proto-api-example-in-ruby 3 | # 4 | class BlueSky 5 | BASE_URL = "https://bsky.social/xrpc" 6 | TOKEN_CACHE_KEY = :bluesky_token_data 7 | 8 | def self.username 9 | if Rails.env.development? || Rails.env.test? 10 | "ottwatch-test.bsky.social" 11 | else 12 | "ottwatch.bsky.social" 13 | end 14 | end 15 | 16 | def self.password 17 | raise "BLUE_SKY_PASSWORD not set" if ENV["BLUE_SKY_PASSWORD"].nil? 18 | ENV["BLUE_SKY_PASSWORD"] 19 | end 20 | 21 | def initialize 22 | @username = BlueSky.username 23 | @password = BlueSky.password 24 | token_data = Rails.cache.read(TOKEN_CACHE_KEY) 25 | process_tokens(token_data) if token_data.present? 26 | end 27 | 28 | def skeet(message) 29 | verify_tokens 30 | facets = link_facets(message) 31 | facets += tag_facets(message) 32 | body = { 33 | repo: @user_did, collection: "app.bsky.feed.post", 34 | record: { text: message, createdAt: Time.now.iso8601, langs: ["en"], facets: facets } 35 | } 36 | post_request("#{BASE_URL}/com.atproto.repo.createRecord", body: body) 37 | end 38 | 39 | private 40 | 41 | def link_facets(message) 42 | [].tap do |facets| 43 | matches = [] 44 | message.scan(URI::RFC2396_PARSER.make_regexp(["http", "https"])) { matches << Regexp.last_match } 45 | matches.each do |match| 46 | start, stop = match.byteoffset(0) 47 | facets << { 48 | "index" => { "byteStart" => start, "byteEnd" => stop }, 49 | "features" => [{ "uri" => match[0], "$type" => "app.bsky.richtext.facet#link" }] 50 | } 51 | end 52 | end 53 | end 54 | 55 | def tag_facets(message) 56 | [].tap do |facets| 57 | matches = [] 58 | message.scan(/(^|[^\w])(#[\w\-]+)/) { matches << Regexp.last_match } 59 | matches.each do |match| 60 | start, stop = match.byteoffset(2) 61 | facets << { 62 | "index" => { "byteStart" => start, "byteEnd" => stop }, 63 | "features" => [{ "tag" => match[2].delete_prefix("#"), "$type" => "app.bsky.richtext.facet#tag" }] 64 | } 65 | end 66 | end 67 | end 68 | 69 | def post_request(url, body: {}, auth_token: true, content_type: "application/json") 70 | uri = URI.parse(url) 71 | http = Net::HTTP.new(uri.host, uri.port) 72 | http.use_ssl = (uri.scheme == "https") 73 | http.open_timeout = 4 74 | http.read_timeout = 4 75 | http.write_timeout = 4 76 | request = Net::HTTP::Post.new(uri.request_uri) 77 | request["content-type"] = content_type 78 | 79 | if auth_token 80 | token = auth_token.is_a?(String) ? auth_token : @token 81 | request["Authorization"] = "Bearer #{token}" 82 | end 83 | request.body = body.is_a?(Hash) ? body.to_json : body if body.present? 84 | response = http.request(request) 85 | raise "#{response.code} response - #{response.body}" unless response.code.to_s.start_with?("2") 86 | 87 | response.content_type == "application/json" ? JSON.parse(response.body) : response.body 88 | end 89 | 90 | def generate_tokens 91 | body = { identifier: @username, password: @password } 92 | response_body = post_request("#{BASE_URL}/com.atproto.server.createSession", body: body, auth_token: false) 93 | 94 | process_tokens(response_body) 95 | store_token_data(response_body) 96 | end 97 | 98 | def perform_token_refresh 99 | response_body = post_request("#{BASE_URL}/com.atproto.server.refreshSession", auth_token: @renewal_token) 100 | 101 | process_tokens(response_body) 102 | store_token_data(response_body) 103 | end 104 | 105 | def verify_tokens 106 | if @token.nil? 107 | generate_tokens 108 | elsif @token_expires_at < Time.now.utc + 60 109 | perform_token_refresh 110 | end 111 | end 112 | 113 | def process_tokens(response_body) 114 | @token = response_body["accessJwt"] 115 | @renewal_token = response_body["refreshJwt"] 116 | @user_did = response_body["did"] 117 | @token_expires_at = Time.at(JSON.parse(Base64.decode64(response_body["accessJwt"].split(".")[1]))["exp"]).utc 118 | end 119 | 120 | def store_token_data(data) 121 | Rails.cache.write(TOKEN_CACHE_KEY, data) 122 | end 123 | end -------------------------------------------------------------------------------- /app/models/committee.rb: -------------------------------------------------------------------------------- 1 | class Committee < ApplicationRecord 2 | end 3 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinodotnet/ottwatch/2e337b8583bb932234052e6c6a3a4aac516bcd5a/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/consultation.rb: -------------------------------------------------------------------------------- 1 | class Consultation < ApplicationRecord 2 | has_many :announcements, as: :reference 3 | 4 | def full_href 5 | "https://engage.ottawa.ca#{href}" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/models/consultation_scanner.rb: -------------------------------------------------------------------------------- 1 | class ConsultationScanner < ApplicationJob 2 | queue_as :default 3 | 4 | def perform 5 | data = Net::HTTP.get(URI("https://engage.ottawa.ca/projects")) 6 | doc = Nokogiri::HTML(data) 7 | 8 | # Parse for... 9 | #
10 | #
11 | # Published 12 | #
13 | # 14 | #
15 | #
16 | # Quinn Farm Park 17 | #
18 | #
19 | #
20 | # 21 | # And MAP to: 22 | # {:title=>"Meadowbrook Park (Rideau) ", :state=>"published", :href=>"/meadowbrook-park-rideau"}, 23 | 24 | tiles = doc.xpath("//div").map do |d| 25 | next unless d.attributes["class"]&.value 26 | next unless d.attributes["class"]&.value.split(" ").include?("project-tile") 27 | tile = Nokogiri::HTML(d.to_s) 28 | title = tile.xpath("//body/div").first.attributes["data-name"].value.humanize 29 | title = title.split.map(&:capitalize).join(' ') 30 | href = tile.xpath('//a[@class="project-tile__link"]').first.attributes["href"].value 31 | status = d.attributes["data-state"].value 32 | { 33 | title: title, 34 | href: href, 35 | status: status, 36 | } 37 | end.compact 38 | 39 | Consultation.transaction do 40 | tiles.each do |t| 41 | Consultation.where(href: t[:href]).first_or_create do |c| 42 | c.title = t[:title] 43 | c.status = t[:status] 44 | c.announcements << Announcement.new(message: "New Consultation: #{c.title}") 45 | end 46 | end 47 | end 48 | 49 | nil 50 | end 51 | end -------------------------------------------------------------------------------- /app/models/coordinates.rb: -------------------------------------------------------------------------------- 1 | class Coordinates 2 | def initialize(lat, lon, precision: 5) 3 | @precision = precision 4 | @full_lat = lat 5 | @full_lon = lon 6 | # Accuracy | DD.dddddd° Decimal places 7 | # 10m | 4 8 | # 1m | 5 9 | # 0.1m | 6 10 | # https://wiki.openstreetmap.org/wiki/Precision_of_coordinates 11 | @lat = lat.round(precision) 12 | @lon = lon.round(precision) 13 | end 14 | 15 | attr_reader :lat, :lon, :full_lat, :full_lon, :precision 16 | end 17 | -------------------------------------------------------------------------------- /app/models/dev_app.rb: -------------------------------------------------------------------------------- 1 | module DevApp 2 | def self.table_name_prefix 3 | "dev_app_" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/models/dev_app/address.rb: -------------------------------------------------------------------------------- 1 | class DevApp::Address < ApplicationRecord 2 | belongs_to :entry, class_name: "DevApp::Entry" 3 | 4 | def coordinates 5 | Coordinates.new(lat, lon) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/models/dev_app/document.rb: -------------------------------------------------------------------------------- 1 | class DevApp::Document < ApplicationRecord 2 | belongs_to :entry, class_name: "DevApp::Entry", foreign_key: "entry_id" 3 | end 4 | -------------------------------------------------------------------------------- /app/models/dev_app/entry.rb: -------------------------------------------------------------------------------- 1 | class DevApp::Entry < ApplicationRecord 2 | has_many :statuses, class_name: "DevApp::Status" 3 | has_many :addresses, class_name: "DevApp::Address" 4 | has_many :documents, class_name: "DevApp::Document" 5 | has_many :announcements, as: :reference 6 | 7 | def current_status 8 | if statuses.none? 9 | # likely due to missing ottawa.ca data, some entries never got an "at least one" status 10 | # recorded. At runtime, avoid this by returning a mock/stub/placeholder and also persist 11 | # it so UI elements that power themselves from DB enums on that column continue to work. 12 | statuses.create(status: "404_missing_data") 13 | else 14 | statuses.last 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/models/dev_app/status.rb: -------------------------------------------------------------------------------- 1 | class DevApp::Status < ApplicationRecord 2 | belongs_to :entry, class_name: "DevApp::Entry" 3 | end 4 | -------------------------------------------------------------------------------- /app/models/global_control.rb: -------------------------------------------------------------------------------- 1 | class GlobalControl < ApplicationRecord 2 | def self.get(name) 3 | find_by(name: name)&.value 4 | end 5 | 6 | def self.set(name, value) 7 | gc = find_by(name: name) || GlobalControl.new(name: name) 8 | gc.value = value 9 | gc.save! 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/models/lobbying_activity.rb: -------------------------------------------------------------------------------- 1 | class LobbyingActivity < ApplicationRecord 2 | belongs_to :lobbying_undertaking 3 | end 4 | -------------------------------------------------------------------------------- /app/models/lobbying_undertaking.rb: -------------------------------------------------------------------------------- 1 | class LobbyingUndertaking < ApplicationRecord 2 | has_many :activities, class_name: "LobbyingActivity" 3 | has_many :announcements, as: :reference 4 | end 5 | -------------------------------------------------------------------------------- /app/models/mastedon_client.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | require 'uri' 3 | require 'json' 4 | 5 | class MastedonClient 6 | def initialize 7 | end 8 | 9 | def self.update(message) 10 | true 11 | 12 | # base_url = "https://urbanists.social/" 13 | # access_token = ENV.fetch("MASTEDON_ACCESS_TOKEN") 14 | 15 | # uri = URI("#{base_url}/api/v1/statuses") 16 | # req = Net::HTTP::Post.new(uri) 17 | # req.set_form_data('status' => message) 18 | # req['Authorization'] = "Bearer #{access_token}" 19 | 20 | # http = Net::HTTP.new(uri.host, uri.port) 21 | # http.use_ssl = true 22 | # res = http.request(req) 23 | 24 | # res.body 25 | end 26 | end 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/models/meeting.rb: -------------------------------------------------------------------------------- 1 | class Meeting < ApplicationRecord 2 | belongs_to :committee 3 | has_many :announcements, as: :reference 4 | has_many :items, class_name: "MeetingItem" 5 | end 6 | -------------------------------------------------------------------------------- /app/models/meeting_item.rb: -------------------------------------------------------------------------------- 1 | class MeetingItem < ApplicationRecord 2 | belongs_to :meeting 3 | has_many :documents, class_name: "MeetingItemDocument" 4 | end 5 | -------------------------------------------------------------------------------- /app/models/meeting_item_document.rb: -------------------------------------------------------------------------------- 1 | class MeetingItemDocument < ApplicationRecord 2 | belongs_to :meeting_item 3 | end 4 | -------------------------------------------------------------------------------- /app/models/parcel.rb: -------------------------------------------------------------------------------- 1 | class Parcel < ApplicationRecord 2 | def center 3 | t = tmp_polygon 4 | [ 5 | t.map{|p| p[0]}.sum / t.count, 6 | t.map{|p| p[1]}.sum / t.count, 7 | ] 8 | end 9 | 10 | def tmp_polygon 11 | JSON.parse(geometry_json)["rings"].first.map do |p| 12 | x = p[0] 13 | y = p[1] 14 | 15 | return nil if x.abs < 180 && y.abs < 90 16 | # 20037508.3427892 - is the full extent of web mercator 17 | return nil if x.abs > 20037508.3427892 || y.abs > 20037508.3427892 18 | 19 | num3 = x / 6378137.0; 20 | # 57.29 = 180/pi 21 | num4 = num3 * 57.295779513082323; 22 | num5 = (((num4 + 180.0) / 360.0)).floor; 23 | num6 = num4 - (num5 * 360.0); 24 | num7 = 1.5707963267948966 - (2.0 * Math.atan(Math.exp((-1.0 * y) / 6378137.0))); 25 | 26 | lon = num6; 27 | lat = num7 * 57.295779513082323; 28 | 29 | [lat, lon] 30 | end 31 | end 32 | end -------------------------------------------------------------------------------- /app/models/parcel_scanner.rb: -------------------------------------------------------------------------------- 1 | class ParcelScanner < ApplicationJob 2 | queue_as :default 3 | 4 | def perform(allow_again: true) 5 | objectid = Parcel.where(snapshot_date: current_month).maximum(:objectid) || 0 6 | again = false 7 | objects_after(objectid).each do |feature| 8 | parcel_from_api(feature) 9 | again = true 10 | end 11 | ParcelScanner.perform_later if allow_again && again 12 | end 13 | 14 | def objects_after(objectid) 15 | json = maps_ottawa_http_get("https://maps.ottawa.ca/proxy/proxy.ashx?https://maps.ottawa.ca/arcgis/rest/services/Property_Parcels/MapServer/2/query?where=OBJECTID>#{objectid}&orderByFields=OBJECTID&outFields=*&f=json") 16 | result = JSON.parse(json) 17 | result.fetch("features") 18 | end 19 | 20 | private 21 | 22 | def current_month 23 | Date.today.strftime("%Y-%m-01").to_date 24 | end 25 | 26 | def parcel_from_api(feature) 27 | attributes = feature.dig("attributes").map{|k, v| [k.downcase, v.to_s.gsub(/ *$/, '')]}.to_h 28 | attributes = attributes.except("textheight", "textwidth", "textrotation") 29 | parcel = Parcel.find_or_create_by(snapshot_date: current_month, objectid: attributes["objectid"]) 30 | parcel.assign_attributes(attributes) 31 | parcel.geometry_json = feature["geometry"].to_json 32 | parcel.save! 33 | parcel 34 | end 35 | 36 | def maps_ottawa_http_get(url) 37 | uri = URI(url) 38 | req = Net::HTTP::Get.new(uri) 39 | req['Referer'] = "https://maps.ottawa.ca/wab_stemapp/geoOttawaReport/index_en.html?report=parcelReport" 40 | res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') { |http| 41 | http.request(req) 42 | } 43 | res.body 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /app/models/traffic_camera.rb: -------------------------------------------------------------------------------- 1 | class TrafficCamera < ApplicationRecord 2 | CAPTURE_FOLDER = (ENV["LOCAL_STORAGE_FOLDER"] || Rails.root.join("tmp").to_s) + "/camera" 3 | SQLITE_ARCHIVE = "#{CAPTURE_FOLDER}/camera_archive.sqlar" 4 | 5 | def self.cameras 6 | @cameras ||= begin 7 | data = Net::HTTP.get(URI("https://traffic.ottawa.ca/service/camera")) 8 | JSON.parse(data)["cameras"].map(&:with_indifferent_access) 9 | end 10 | end 11 | 12 | def self.scrape_all 13 | cameras.each do |camera| 14 | tc = TrafficCamera.find_or_create_by!(reference_id: camera[:id]) 15 | tc.update!( 16 | lat: camera[:latitude], 17 | lon: camera[:longitude], 18 | name: camera[:name], 19 | camera_owner: camera[:cameraOwner], 20 | camera_number: camera[:camera_number] 21 | ) 22 | end 23 | end 24 | 25 | def current_image_url 26 | time_now = (Time.now.to_f * 1000).to_i 27 | "https://traffic.ottawa.ca/camera?id=#{camera_number}&timems=#{time_now}" 28 | end 29 | 30 | def capture_image 31 | time_now = (Time.now.to_f * 1000).to_i 32 | response = Net::HTTP.get(URI(current_image_url)) 33 | camera_path = "#{CAPTURE_FOLDER}/#{id}" 34 | capture_filename = "#{camera_path}/#{id}_#{time_now}.jpg" 35 | FileUtils.mkdir_p(camera_path) 36 | File.binwrite(capture_filename, response) 37 | save_image_to_sqlite_archive(time: time_now, image: response) 38 | { 39 | time: time_now, 40 | camera_id: id, 41 | filename: capture_filename, 42 | image: response 43 | } 44 | end 45 | 46 | def save_image_to_sqlite_archive(time:, image:) 47 | filename = "#{id}_#{time}.jpg" 48 | file_data = nil # File.binread(filename) 49 | file_stat = nil # File.stat(filename) 50 | 51 | SQLite3::Database.open(SQLITE_ARCHIVE) do |db| 52 | db.execute( 53 | "INSERT INTO sqlar (name, mode, mtime, sz, data) VALUES (?, ?, ?, ?, ?)", 54 | [filename, file_stat&.mode, file_stat&.mtime.to_i, image.size, image] 55 | ) 56 | end 57 | end 58 | 59 | def image_from_sqlite_archive(time_ms) 60 | SQLite3::Database.open(SQLITE_ARCHIVE) do |db| 61 | db.execute("SELECT data FROM sqlar WHERE name = ?", ["#{id}_#{time_ms}.jpg"]) do |row| 62 | return row.first 63 | end 64 | end 65 | nil 66 | end 67 | 68 | def capture_jpg(time_ms) 69 | c = captures.detect { |capture| capture[:time_ms] == time_ms } 70 | return unless c 71 | File.read(c[:file]) 72 | end 73 | 74 | def captures 75 | Dir.glob(File.join(CAPTURE_FOLDER, id.to_s, '**', '*')).select { |f| File.file?(f) }.sort.map do |file| 76 | time_ms = file.scan(/.*#{id}\/#{id}_(\d+)\.jpg/).first.first.to_i 77 | time = time_ms / 1000 78 | { 79 | camera: self, 80 | time_ms: time_ms, 81 | time: time, 82 | file: file 83 | } 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ApplicationRecord 2 | # Include default devise modules. Others available are: 3 | # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable 4 | devise :database_authenticatable, :registerable, 5 | :recoverable, :rememberable, :validatable, 6 | :trackable, 7 | :omniauthable, omniauth_providers: %i[google_oauth2 github] 8 | 9 | def email_required? 10 | true 11 | end 12 | 13 | def self.from_omniauth(auth) 14 | where(provider: auth.provider, uid: auth.uid).first_or_create do |user| 15 | user.email = auth.info.email 16 | user.password = Devise.friendly_token[0, 20] 17 | user.name = auth.info.name 18 | user.username = auth.info.nickname || user.name || user.email 19 | end 20 | end 21 | end 22 | 23 | -------------------------------------------------------------------------------- /app/models/v1/application_record.rb: -------------------------------------------------------------------------------- 1 | module V1 2 | class ApplicationRecord < ::ApplicationRecord 3 | self.abstract_class = true 4 | 5 | connects_to database: { writing: :v1, reading: :v1 } 6 | end 7 | end -------------------------------------------------------------------------------- /app/models/zoning.rb: -------------------------------------------------------------------------------- 1 | class Zoning < ApplicationRecord 2 | end 3 | -------------------------------------------------------------------------------- /app/models/zoning_scanner.rb: -------------------------------------------------------------------------------- 1 | class ZoningScanner < ApplicationJob 2 | queue_as :default 3 | 4 | TRANSLATIONS = { 5 | "shape.area" => "shape_area", 6 | "shape.len" => "shape_length", 7 | "tooltip_en" => nil, 8 | "tooltip_fr" => nil, 9 | "url_fr" => nil, 10 | } 11 | 12 | def perform(allow_again: true) 13 | objectid = Zoning.where(snapshot_date: current_month).maximum(:objectid) || 0 14 | again = false 15 | objects_after(objectid).each do |feature| 16 | zoning_from_api(feature) 17 | again = true 18 | end 19 | ZoningScanner.perform_later if allow_again && again 20 | end 21 | 22 | def objects_after(objectid) 23 | json = maps_ottawa_http_get("https://maps.ottawa.ca/arcgis/rest/services/Zoning/MapServer/3/query?where=OBJECTID>#{objectid}&orderByFields=OBJECTID&outFields=*&f=json") 24 | result = JSON.parse(json) 25 | result.fetch("features") 26 | end 27 | 28 | private 29 | 30 | def current_month 31 | Date.today.strftime("%Y-%m-01").to_date 32 | end 33 | 34 | def zoning_from_api(feature) 35 | orig_attr = feature.dig("attributes").map{|k, v| [k.downcase, v.to_s.gsub(/ *$/, '')]}.to_h 36 | 37 | attributes = orig_attr.except(*TRANSLATIONS.keys) 38 | TRANSLATIONS.each do |k,v| 39 | next if v.nil? 40 | attributes[v] = orig_attr[k] 41 | end 42 | 43 | zoning = Zoning.find_or_create_by(snapshot_date: current_month, objectid: attributes["objectid"]) do |zoning| 44 | zoning.assign_attributes(attributes) 45 | zoning.geometry_json = feature["geometry"].to_json 46 | end 47 | end 48 | 49 | def maps_ottawa_http_get(url) 50 | uri = URI(url) 51 | req = Net::HTTP::Get.new(uri) 52 | # req['Referer'] = "https://maps.ottawa.ca/wab_stemapp/geoOttawaReport/index_en.html?report=parcelReport" 53 | res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') { |http| 54 | http.request(req) 55 | } 56 | res.body 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /app/views/announcement/index.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | last_key = params["reference_type"] || "all" 3 | last_id = session["annoucements_index_#{last_key}"] || 0 4 | session["annoucements_index_#{last_key}"] = @announcements.first&.id 5 | %> 6 |
7 |
8 |

9 | Announcements 10 | 11 | <%= link_to("all", announcement_index_path) %> 12 | <%= link_to("devapps", announcement_index_path(reference_type: "DevApp::Entry")) %> 13 | <%= link_to("meetings", announcement_index_path(reference_type: "Meeting")) %> 14 | <%= link_to("consultations", announcement_index_path(reference_type: "Consultation")) %> 15 | <%= link_to("lobbying", announcement_index_path(reference_type: "LobbyingUndertaking")) %> 16 | 17 |

18 |
19 | 20 |
21 | Next: 22 | <% [25, 50, 100].each do |limit| %> 23 | <%= link_to( 24 | "#{limit}", 25 | announcement_index_path( 26 | before_id: @announcements.last&.id, 27 | reference_type: params[:reference_type], 28 | limit: limit 29 | ) 30 | ) %> 31 | <% end %> 32 |
33 |
34 | 35 | <% 36 | prev_date = nil 37 | @announcements.each do |a| 38 | is_new_date = a.created_at.to_date != prev_date 39 | if is_new_date 40 | %> 41 |
<%= a.created_at.in_time_zone("America/New_York").strftime("%Y-%m-%d") %>
42 | <% 43 | end 44 | prev_date = a.created_at.to_date 45 | message = a.message 46 | message = [a.message, a.reference_context].join(" ") if a.reference_type == "LobbyingUndertaking" 47 | %> 48 |
49 |
50 | 51 | <%= link_to(message, a.reference_link) %> 52 |
53 |
54 | <%= raw("(new)") if a.id > last_id %> 55 |
56 |
57 | <% end %> 58 | 59 | -------------------------------------------------------------------------------- /app/views/devapp/_map_popup.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |
7 |

8 | 9 |

10 |

11 | View Details 12 |
13 |
14 |
15 | -------------------------------------------------------------------------------- /app/views/devapp/index.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Development Applications

4 |
5 | <%= link_to "List", devapp_index_path, class: "btn btn-outline-primary active" %> 6 | <%= link_to "Map", devapp_map_path, class: "btn btn-outline-primary" %> 7 |
8 |
9 | 10 |
11 | <% @devapps.each do |d| %> 12 |
13 |
14 | <%= link_to "/devapp/#{d.app_number}" do %> 15 |
<%= d.app_number %>
16 | <% end %> 17 | <%= d.updated_at.strftime("%B %d, %Y") %> 18 |
19 |

<%= d.app_type %>

20 |

<%= d.desc %>

21 | 22 | Addresses: 23 | <%= d.addresses.reject { |a| a.ref_id.blank? }.map { |a| [a.road_number, a.road_name, a.direction, a.road_type].compact.join(" ") }.join(", ") %> 24 | 25 |
26 | <% end %> 27 |
28 | 29 |
30 | <%= link_to "Load More", devapp_index_path(before_id: @devapps.last&.id), class: "btn btn-outline-primary" %> 31 |
32 |
33 | -------------------------------------------------------------------------------- /app/views/devapp/map.html.erb: -------------------------------------------------------------------------------- 1 |
6 |
7 |

Development Applications

8 |
9 | <%= link_to "List", devapp_index_path, class: "btn btn-outline-primary" %> 10 | <%= link_to "Map", devapp_map_path, class: "btn btn-outline-primary active" %> 11 |
12 |
13 | 14 |
15 |
16 |
17 |
18 |
Filters
19 |
20 |
21 |
22 | <% { status: @statuses, app_type: @app_types }.each do |group_name, options| %> 23 |
24 |
<%= group_name.to_s.titleize %>
25 | <% options.each do |option| %> 26 |
27 | 33 | 36 |
37 | <% end %> 38 |
39 | <% end %> 40 | 43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | 52 | <%= render partial: 'map_popup' %> 53 |
54 | 55 | -------------------------------------------------------------------------------- /app/views/devapp/show.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for(:html_title, "#{@entry.app_number} - Development Application") %> 2 | 3 |
4 |

<%= @entry.app_number %>

5 | 6 |
7 |
8 |
9 |
10 |
Application Details
11 |
12 |
Number
13 |
<%= @entry.app_number %>
14 | 15 |
Type
16 |
<%= @entry.app_type %>
17 | 18 |
Indexed
19 |
<%= @entry.created_at.strftime("%B %d, %Y") %>
20 | 21 |
Description
22 |
<%= @entry.desc %>
23 | 24 |
External Link
25 |
26 | <%= link_to("View application on ottawa.ca", "https://devapps.ottawa.ca/en/applications/#{@entry.app_number}/details", target: "_blank", class: "btn btn-sm btn-outline-primary") %> 27 |
28 |
29 |
30 |
31 | 32 |
33 |
34 |
Status History
35 |
    36 | <% @entry.statuses.each do |status| %> 37 |
  • 38 | <%= status.created_at.strftime("%B %d, %Y") %>: <%= status.status %> 39 |
  • 40 | <% end %> 41 |
42 |
43 |
44 | 45 |
46 |
47 |
Addresses
48 |
    49 | <% @entry.addresses.each do |address| %> 50 |
  • 51 | <%= [address.road_number, address.road_name, address.road_type, address.direction, address.municipality].compact.join(" ") %> 52 |
  • 53 | <% end %> 54 |
55 |
56 |
57 | 58 |
59 |
60 |
City Staff Contact
61 |

62 | <%= @entry.planner_first_name %> <%= @entry.planner_last_name %>
63 | <%= @entry.planner_email %>
64 | <%= @entry.planner_phone %> 65 |

66 |
67 |
68 | 69 |
70 |
71 |
Documents
72 |
    73 | <% @entry.documents.each do |document| %> 74 |
  • 75 | <%= link_to(document.name.gsub(@entry.app_number, "").strip, document.url, target: "_blank") %> 76 | <% unless %w(200 302).include?(document.state) %> 77 | Missing/broken document: <%= document.state %> 78 | <% end %> 79 |
  • 80 | <% end %> 81 |
82 |
83 |
84 |
85 | 86 |
87 |
94 |
95 |
96 |
97 | -------------------------------------------------------------------------------- /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/edit.html.erb: -------------------------------------------------------------------------------- 1 |

Edit <%= resource_name.to_s.humanize %>

2 | 3 | <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) 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 | <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %> 12 |
Currently waiting confirmation for: <%= resource.unconfirmed_email %>
13 | <% end %> 14 | 15 |
16 | <%= f.label :password %> (leave blank if you don't want to change it)
17 | <%= f.password_field :password, autocomplete: "new-password" %> 18 | <% if @minimum_password_length %> 19 |
20 | <%= @minimum_password_length %> characters minimum 21 | <% end %> 22 |
23 | 24 |
25 | <%= f.label :password_confirmation %>
26 | <%= f.password_field :password_confirmation, autocomplete: "new-password" %> 27 |
28 | 29 |
30 | <%= f.label :current_password %> (we need your current password to confirm your changes)
31 | <%= f.password_field :current_password, autocomplete: "current-password" %> 32 |
33 | 34 |
35 | <%= f.submit "Update" %> 36 |
37 | <% end %> 38 | 39 |

Cancel my account

40 | 41 |

Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %>

42 | 43 | <%= link_to "Back", :back %> 44 | -------------------------------------------------------------------------------- /app/views/devise/registrations/new.html.erb: -------------------------------------------------------------------------------- 1 |

Sign up

2 | 3 | <%= form_for(resource, as: resource_name, url: registration_path(resource_name)) 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.label :password %> 13 | <% if @minimum_password_length %> 14 | (<%= @minimum_password_length %> characters minimum) 15 | <% end %>
16 | <%= f.password_field :password, autocomplete: "new-password" %> 17 |
18 | 19 |
20 | <%= f.label :password_confirmation %>
21 | <%= f.password_field :password_confirmation, autocomplete: "new-password" %> 22 |
23 | 24 |
25 | <%= f.submit "Sign up" %> 26 |
27 | <% end %> 28 | 29 | <%= render "devise/shared/links" %> 30 | -------------------------------------------------------------------------------- /app/views/devise/sessions/new.html.erb: -------------------------------------------------------------------------------- 1 |

Log in

2 | 3 | <%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %> 4 | <% if false %> 5 |
6 | <%= f.label :email %>
7 | <%= f.email_field :email, autofocus: true, autocomplete: "email" %> 8 |
9 | 10 |
11 | <%= f.label :password %>
12 | <%= f.password_field :password, autocomplete: "current-password" %> 13 |
14 | 15 | <% if devise_mapping.rememberable? %> 16 |
17 | <%= f.check_box :remember_me %> 18 | <%= f.label :remember_me %> 19 |
20 | <% end %> 21 | 22 |
23 | <%= f.submit "Log in" %> 24 |
25 | <% end %> 26 | <% end %> 27 | 28 | <%= render "devise/shared/links" %> 29 | -------------------------------------------------------------------------------- /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 | 14 |
15 | <% end %> 16 | -------------------------------------------------------------------------------- /app/views/devise/shared/_links.html.erb: -------------------------------------------------------------------------------- 1 | <%- if false %> 2 | <%- if controller_name != 'sessions' %> 3 | <%= link_to "Log in", new_session_path(resource_name) %>
4 | <% end %> 5 | 6 | <%- if devise_mapping.registerable? && controller_name != 'registrations' %> 7 | <%= link_to "Sign up", new_registration_path(resource_name) %>
8 | <% end %> 9 | 10 | <%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> 11 | <%= link_to "Forgot your password?", new_password_path(resource_name) %>
12 | <% end %> 13 | 14 | <%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> 15 | <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
16 | <% end %> 17 | 18 | <%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> 19 | <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
20 | <% end %> 21 | 22 | <% end %> 23 | 24 | <%- if devise_mapping.omniauthable? %> 25 | <%- resource_class.omniauth_providers.each do |provider| %> 26 | <%= button_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), data: { turbo: false }, method: :post %>
27 | <% end %> 28 | <% end %> 29 | -------------------------------------------------------------------------------- /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/home/index.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Announcements

5 |
6 |
7 |
    8 | <% @announcements.each do |a| %> 9 |
  • 10 | <% 11 | message = a.message 12 | message = [a.message, a.reference_context].join(" ") if a.reference_type == "LobbyingUndertaking" 13 | %> 14 | 15 | <%= link_to(message, a.reference_link) %> 16 |
  • 17 | <% end %> 18 |
  • <%= link_to("... see all", "/announcement/index") %>
  • 19 |
20 |
21 |
22 |
23 |
24 |

Meetings

25 |
26 |
27 |
    28 | <% @meetings.each do |m| %> 29 | <% meeting_url = "/meeting/#{m.reference_id}#{m.reference_guid}" %> 30 |
  • 31 | <%= link_to(m.start_time.in_time_zone("America/New_York").strftime("%b %d @ %H:%M #{m.committee.name}"), meeting_url) %> 32 |
  • 33 | <% end %> 34 |
  • <%= link_to("... see all", "/meeting/index") %>
  • 35 |
36 |
37 |
38 |
39 | 40 | 41 |
42 |

Features

43 |
44 | 45 |
46 |
47 |

48 | <%= link_to "Announcements", announcement_index_path %> are OttWatch's bread and butter. OttWatch scans ottawa.ca for 49 | new information so you don't have to. 50 |

51 |
52 |
53 |

54 | The DevApps area is center stage for all development applications in the city. If someone is building 55 | something you can comment on it to staff before it goes to Committee for approval, and this section is where you can find all details 56 | about what people are planning to do. 57 |

58 |
59 |
60 |

61 | Commmittee meetings are where it's at for influencing the city around you. Yes, you'll have more trouble 62 | having influence at the Ottawa Policy Board 63 | than at city committes, but an opportunity still exists. Don't wait to hear about a city decision after it's been made: attend a meeting 64 | today and have your say. 65 |

66 |
67 |
68 | 69 |
70 |
71 |

72 | To track influence, our lobbying database provides easy access to the city's Lobbyist Registry. 73 | Corporate intersests are always trying to influence the city, so it's good to know who's up to what, so we can influence right back. 74 |

75 |
76 |
77 |

78 | In the less-politics-more-transit department, we have a 79 | <%= link_to "Next Bus", transpo_show_stop_path %> 80 | section. Very simple. No frills. But I personally like it that way. 81 | Google "BusBuddy" for a full featured system, which I also use. 82 |

83 |
84 |
85 |

86 | 87 | Catch the old-school RSS feed: you know what to do. 88 |

89 |
90 |
-------------------------------------------------------------------------------- /app/views/home/index.rss.builder: -------------------------------------------------------------------------------- 1 | xml.instruct! :xml, :version => "1.0" 2 | xml.rss :version => "2.0" do 3 | xml.channel do 4 | xml.title "OttWatch Announcements" 5 | xml.description "Latest announcements" 6 | xml.link root_url 7 | 8 | @announcements.each do |a| 9 | xml.item do 10 | xml.title a.message 11 | xml.description "#{a.reference_context}: #{a.reference.class.name}" 12 | xml.pubDate a.created_at.to_fs(:rfc822) 13 | xml.link a.reference_link 14 | xml.guid "https://ottwatch.ca/announcement/#{a.id}" 15 | end 16 | end 17 | end 18 | end -------------------------------------------------------------------------------- /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/lobbying/index.html.erb: -------------------------------------------------------------------------------- 1 |

Lobbying Activities

2 | 3 |
4 | <% @undertakings.each do |u| %> 5 |
6 |
7 | <%= u.client %> (by <%= u.lobbyist_name %>) 8 |
9 |
10 | <%= link_to u.subject, "/lobbying/#{u.id}" %>: <%= u.issue %> 11 |
12 |
13 | Latest: <%= @latest[u.id].strftime("%Y-%m-%d") %>
14 | First: <%= @first[u.id].strftime("%Y-%m-%d") %>
15 | Total: <%= @counts[u.id] %>
16 |
17 |
18 | <% end %> 19 |
20 | 21 | <% if @undertakings.count > 0 %> 22 |
23 | <%= link_to "More...", lobbying_index_path(before_id: @undertakings.last.id, before_created_at: @undertakings.last.created_at) %> 24 |
25 | <% end %> -------------------------------------------------------------------------------- /app/views/lobbying/show.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for(:html_title, "Lobbying: #{@undertaking.client_org}") %> 2 | 3 |   4 | 5 |
6 |
7 |

Topic/Issue

8 |

9 | <%= @undertaking.issue %>
10 | (<%= @undertaking.subject %>) 11 |

12 |
13 |
14 |

Lobbyist

15 | Client: <%= @undertaking.client %> (<%= @undertaking.client_org %>)
16 | Lobbyist: <%= @undertaking.lobbyist_name %> (<%= @undertaking.lobbyist_position %>, <%= @undertaking.lobbyist_reg_type %>)
17 |
18 |
19 | 20 |   21 | 22 |
23 |
24 |

Activities

25 |
26 |
27 | 28 |
29 |
Who was lobbied?
30 |
Their title
31 |
When?
32 |
By?
33 |
34 | <% @undertaking.activities.order(activity_date: :desc).each do |a| %> 35 |
36 |
<%= a.lobbied_name %>
37 |
<%= a.lobbied_title %>
38 |
<%= a.activity_date %>
39 |
<%= a.activity_type %>
40 |
41 | <% end %> 42 | 43 |   44 | 45 | 46 |

External links

47 | 48 |
49 | <% 50 | j = JSON.parse(@undertaking.view_details) 51 | j.keys.each do |k| 52 | %> 53 | 54 | <% 55 | end 56 | %> 57 | 58 |
59 | 60 | 61 |   -------------------------------------------------------------------------------- /app/views/meeting/index.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <% unless params["before_id"] %> 3 |

Today's Meetings

4 | <% if @today.none? %> 5 |
6 |
7 | No meetings today 8 |
9 |
10 | <% end %> 11 | <% @today.each do |m| %> 12 | <% meeting_url = "/meeting/#{m.reference_id}#{m.reference_guid}" %> 13 |
14 |
15 | <%= link_to(m.start_time.in_time_zone("America/New_York").strftime("%b %d %H:%M, %Y"), meeting_url) %> 16 |
17 |
18 | <%= m.committee.name %> 19 |
20 |
21 | <% end %> 22 |

Upcoming Meetings

23 | <% @upcoming.each do |m| %> 24 | <% meeting_url = "/meeting/#{m.reference_id}#{m.reference_guid}" %> 25 |
26 |
27 | <%= link_to(m.start_time.in_time_zone("America/New_York").strftime("%b %d %H:%M, %Y"), meeting_url) %> 28 |
29 |
30 | <%= m.committee.name %> 31 |
32 |
33 | <% end %> 34 | <% end %> 35 | 36 |

Past Meetings

37 | <% @previous.each do |m| %> 38 | <% meeting_url = "/meeting/#{m.reference_id}#{m.reference_guid}" %> 39 |
40 |
41 | <%= link_to(m.start_time.in_time_zone("America/New_York").strftime("%b %d %H:%M, %Y"), meeting_url) %> 42 |
43 |
44 | <%= m.committee.name %> 45 |
46 |
47 | <% end %> 48 |
49 | 50 |
51 |
52 | <% if @previous.any? %> 53 | <%= link_to "More...", meeting_index_path(before_id: @previous.last.id) %> 54 | <% end %> 55 |
56 |
-------------------------------------------------------------------------------- /app/views/meeting/show.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | <%= @meeting.start_time.in_time_zone("America/New_York").strftime("%B %d - %H:%M")%> 4 |
5 |
6 |

<%= @meeting.committee.name %>

7 |
8 |
9 | <% 10 | meeting_url = if @meeting.reference_id 11 | "https://app05.ottawa.ca/sirepub/mtgviewer.aspx?meetid=#{@meeting.reference_id}&doctype=AGENDA" 12 | else 13 | "https://pub-ottawa.escribemeetings.com/Meeting.aspx?Id=#{@meeting.reference_guid}&Agenda=Agenda&lang=English" 14 | end 15 | %> 16 | 17 | View on ottawa.ca 18 |
19 |
20 | 21 | <% 22 | @meeting.items.each do |item| 23 | %> 24 |
25 | 26 | <%= item.title %> 27 | 28 |
29 | <% 30 | if item.content.present? 31 | %> 32 | <%= item.content[0..500] %><%= item.content.size > 500 ? "... (cont)" : "" %> 33 | <% 34 | end 35 | %> 36 | 47 | <% 48 | end 49 | 50 | %> 51 | 52 | <% if @meeting.reference_id %> 53 |

Agenda

54 | (note: this section doesn't work on mobile; blame the city) 55 | 59 | <% end %> 60 | 61 | <% if @meeting.reference_guid %> 62 |

Agenda

63 | 67 | <% end %> 68 | -------------------------------------------------------------------------------- /app/views/parcels/show.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 | <% 7 | @parcel.attributes.each do |k,v| 8 | %> 9 | 10 | 11 | 12 | 13 | <% 14 | end 15 | %> 16 |
<%= k %><%= v %>
17 | 18 | 19 | 62 | 63 | <%= javascript_include_tag "https://maps.googleapis.com/maps/api/js?key=#{ENV["GOOGLE_MAPS_API_KEY"]}&callback=initMap", defer: true %> 64 | 65 | -------------------------------------------------------------------------------- /app/views/team/index.html.erb: -------------------------------------------------------------------------------- 1 |

2 | Congrats @<%= current_user.username %>
3 | You're a member of the team! 4 |

5 | 6 |

7 | How many users are there?
8 | There are <%= User.count %> users. 9 |

10 | 11 |

12 | How long since the latest user joined?
13 | The last user joined at <%= User.last.created_at %> (<%= ((Time.now - User.last.created_at).seconds / 60 ).round(2) %> minutes ago) 14 |

15 | 16 |

17 | What's next?
18 |

21 |

-------------------------------------------------------------------------------- /app/views/traffic_cameras/index.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for(:html_title, "Cameras") %> 2 |
3 | <% @traffic_cameras.each do |camera| %> 4 | <%= link_to traffic_camera_path(camera), class: "camera-item" do %> 5 | <%= image_tag camera.current_image_url, class: "img-fluid", id: "camera-#{camera.camera_number}", style: "border: 2px solid black;", loading: "lazy" %> 6 | <% end %> 7 | <% end %> 8 | 70 |
71 | 72 |
73 | 78 | -------------------------------------------------------------------------------- /app/views/traffic_cameras/show.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for(:html_title, "Camera #{@traffic_camera.id} - #{@traffic_camera.name}") %> 2 |
3 |

<%= @traffic_camera.name %>

4 | 5 | <%= image_tag @traffic_camera.current_image_url, id: "live-image", data: { camera_id: @traffic_camera.camera_number }, class: "img-fluid", style: "border: 2px solid black;" %> 6 |
<%= Time.now.in_time_zone("America/New_York").strftime('%Y-%m-%d %H:%M:%S') %>
7 | 8 |
9 |
10 | <% captures = @traffic_camera.captures.sort_by { |c| c[:time_ms] }.last(20) %> 11 |
12 | <% captures.each_with_index do |capture, index| %> 13 |
14 | <%= image_tag traffic_camera_capture_path(@traffic_camera, time_ms: capture[:time_ms]), 15 | alt: "Traffic camera image at #{Time.at(capture[:time]).strftime('%Y-%m-%d %H:%M:%S')}", 16 | class: "camera-image img-fluid", 17 | style: "border: 2px solid black;" 18 | %> 19 |
<%= Time.at(capture[:time]).in_time_zone("America/New_York").strftime('%Y-%m-%d %H:%M:%S') %> (<%= sprintf('%03d', index) %>)
20 |
21 | <% end %> 22 |
23 | 24 | 61 |
62 | -------------------------------------------------------------------------------- /app/views/transpo/show_stop.html.erb: -------------------------------------------------------------------------------- 1 | <% if @stop_data == :api_error %> 2 |
3 | Oh no! The OC Transpo system returned an error when your search was performed! 4 | You can try again. Maybe the stop number doesn't exist? 5 |
6 | <% end %> 7 | 8 | <% if @stop_data.is_a?(Hash) %> 9 | <% content_for :html_title, "#{@stop_data[:no]}: #{@stop_data[:desc]}" %> 10 | 11 |

<%= link_to("#{@stop_data[:no]} #{@stop_data[:desc]}", transpo_show_stop_path(stop_no: params[:stop_no])) %>

12 | <% 13 | @stop_data[:routes].each do |r| 14 | %> 15 |
16 | <%= link_to r[:no], transpo_show_stop_path(stop_no: params[:stop_no], stop_routes: r[:no]) %> 17 | <%= r[:heading] %> 18 |
19 | 37 | <% 38 | end 39 | %> 40 | <% end %> 41 | 42 |

Search

43 | 44 | 45 | <%= form_with(url: transpo_show_stop_path, method: :get) do |form| %> 46 |
47 |
48 | <%= form.label(:stop_no, "Stop:") %> 49 |
50 |
51 | <%= form.text_field(:stop_no, value: params[:stop_no], required: true, class: "form-control") %> 52 |
53 |
54 |
55 |
56 | <%= form.label(:stop_routes, "Route(s):") %> 57 |
58 |
59 | <%= form.text_field(:stop_routes, value: params[:stop_routes], class: "form-control") %> 60 |
61 |
62 |
63 |
64 |
65 |
66 | <%= form.submit("Search", class: "btn btn-primary") %> 67 |
68 |
69 | 70 | 71 | 72 |
Instructions
73 |

74 | Enter a stop number ("4940") and optionally one or more route numbers ("11 59") separated 75 | by spaces to get the next available trips in all directions. If you leave the route number(s) 76 | empty, all routes servicing that stop will be shown. 77 |

78 | 79 | <% end %> -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby2.7 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'bundle' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "rubygems" 12 | 13 | m = Module.new do 14 | module_function 15 | 16 | def invoked_as_script? 17 | File.expand_path($0) == File.expand_path(__FILE__) 18 | end 19 | 20 | def env_var_version 21 | ENV["BUNDLER_VERSION"] 22 | end 23 | 24 | def cli_arg_version 25 | return unless invoked_as_script? # don't want to hijack other binstubs 26 | return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` 27 | bundler_version = nil 28 | update_index = nil 29 | ARGV.each_with_index do |a, i| 30 | if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN 31 | bundler_version = a 32 | end 33 | next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ 34 | bundler_version = $1 35 | update_index = i 36 | end 37 | bundler_version 38 | end 39 | 40 | def gemfile 41 | gemfile = ENV["BUNDLE_GEMFILE"] 42 | return gemfile if gemfile && !gemfile.empty? 43 | 44 | File.expand_path("../../Gemfile", __FILE__) 45 | end 46 | 47 | def lockfile 48 | lockfile = 49 | case File.basename(gemfile) 50 | when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) 51 | else "#{gemfile}.lock" 52 | end 53 | File.expand_path(lockfile) 54 | end 55 | 56 | def lockfile_version 57 | return unless File.file?(lockfile) 58 | lockfile_contents = File.read(lockfile) 59 | return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ 60 | Regexp.last_match(1) 61 | end 62 | 63 | def bundler_requirement 64 | @bundler_requirement ||= 65 | env_var_version || cli_arg_version || 66 | bundler_requirement_for(lockfile_version) 67 | end 68 | 69 | def bundler_requirement_for(version) 70 | return "#{Gem::Requirement.default}.a" unless version 71 | 72 | bundler_gem_version = Gem::Version.new(version) 73 | 74 | requirement = bundler_gem_version.approximate_recommendation 75 | 76 | return requirement unless Gem.rubygems_version < Gem::Version.new("2.7.0") 77 | 78 | requirement += ".a" if bundler_gem_version.prerelease? 79 | 80 | requirement 81 | end 82 | 83 | def load_bundler! 84 | ENV["BUNDLE_GEMFILE"] ||= gemfile 85 | 86 | activate_bundler 87 | end 88 | 89 | def activate_bundler 90 | gem_error = activation_error_handling do 91 | gem "bundler", bundler_requirement 92 | end 93 | return if gem_error.nil? 94 | require_error = activation_error_handling do 95 | require "bundler/version" 96 | end 97 | return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) 98 | warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" 99 | exit 42 100 | end 101 | 102 | def activation_error_handling 103 | yield 104 | nil 105 | rescue StandardError, LoadError => e 106 | e 107 | end 108 | end 109 | 110 | m.load_bundler! 111 | 112 | if m.invoked_as_script? 113 | load Gem.bin_path("bundler", "bundle") 114 | end 115 | -------------------------------------------------------------------------------- /bin/dbmysql: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mysql -h $DB_HOST -u $DB_USER --password=$DB_PASS $DB_NAME 4 | -------------------------------------------------------------------------------- /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 | load File.expand_path("spring", __dir__) 3 | APP_PATH = File.expand_path("../config/application", __dir__) 4 | require_relative "../config/boot" 5 | require "rails/commands" 6 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | load File.expand_path("spring", __dir__) 3 | require_relative "../config/boot" 4 | require "rake" 5 | Rake.application.run 6 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | # path to your application root. 5 | APP_ROOT = File.expand_path("..", __dir__) 6 | 7 | def system!(*args) 8 | system(*args) || abort("\n== Command #{args} failed ==") 9 | end 10 | 11 | FileUtils.chdir APP_ROOT do 12 | # This script is a way to set up or update your development environment automatically. 13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 14 | # Add necessary setup steps to this file. 15 | 16 | puts "== Installing dependencies ==" 17 | system! "gem install bundler --conservative" 18 | system("bundle check") || system!("bundle install") 19 | 20 | # puts "\n== Copying sample files ==" 21 | # unless File.exist?("config/database.yml") 22 | # FileUtils.cp "config/database.yml.sample", "config/database.yml" 23 | # end 24 | 25 | puts "\n== Preparing database ==" 26 | system! "bin/rails db:prepare" 27 | 28 | puts "\n== Removing old logs and tempfiles ==" 29 | system! "bin/rails log:clear tmp:clear" 30 | 31 | puts "\n== Restarting application server ==" 32 | system! "bin/rails restart" 33 | end 34 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads Spring without using loading other gems in the Gemfile, in order to be fast. 4 | # It gets overwritten when you run the `spring binstub` command. 5 | 6 | if !defined?(Spring) && [nil, "development", "test"].include?(ENV["RAILS_ENV"]) 7 | require "bundler" 8 | 9 | Bundler.locked_gems.specs.find { |spec| spec.name == "spring" }&.tap do |spring| 10 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path 11 | gem "spring", spring.version 12 | require "spring/binstub" 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative "config/environment" 4 | 5 | run Rails.application 6 | Rails.application.load_server 7 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative "boot" 2 | 3 | require "rails/all" 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | 9 | module Ottwatch 10 | class Application < Rails::Application 11 | # Initialize configuration defaults for originally generated Rails version. 12 | config.load_defaults 7.0 13 | 14 | config.active_storage.variant_processor = :mini_magick 15 | 16 | # Configuration for the application, engines, and railties goes here. 17 | # 18 | # These settings can be overridden in specific environments using the files 19 | # in config/environments, which are processed later. 20 | # 21 | # config.time_zone = "Central Time (US & Canada)" 22 | # config.eager_load_paths << Rails.root.join("extras") 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /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 | development: 6 | adapter: async 7 | 8 | test: 9 | adapter: test 10 | 11 | production: 12 | adapter: solid_cable 13 | connects_to: 14 | database: 15 | writing: primary 16 | polling_interval: 0.1.seconds 17 | message_retention: 1.day 18 | -------------------------------------------------------------------------------- /config/credentials.yml.enc: -------------------------------------------------------------------------------- 1 | K6Acvn8NRN6Nut2HDnZuIX79PLpWpo/66jV6VnvNdLnOgkLDWiUV2yMFge3ee55Ujli85gktZe1Chsg5zkWluP00JzbzmHG1MkPnsCcAGauUMV0OFEA8EFsxeM6g+4zHUBQnV1+DwplPvnUSlFAJcuUQwSiL5HWJ5Imj3eSyusEIYKnEuOdSdJRhsqqHMIBrpai2O3q2Q8gB9UfCImmDMtNB9XYPSDu6nGcL50ly0wA8sLFFg2Nc8R5QytMd4F1J0i9c/Ujj43+oh3Yn19H8iBzHlxqTsA6h7OiYQrLH/brHin02ZzeHBzA66aSlwKeq+4xJxVHKDGNu6j481qzLeZ3sKpwneTUC0swY6EOjqQZS6R4rSwxvtBgsdUWZ/JxCU+F6MdQhbfdY8U0NJMHhLNU7s1CXHc2EYomW--aMi7RpNyJRaqaMq1--sD9biFUKMOLmToLjm5EDqQ== -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite. Versions 3.8.0 and up are supported. 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem "sqlite3" 6 | # 7 | default: &default 8 | adapter: mysql2 9 | encoding: utf8mb4 10 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 11 | username: root 12 | password: 13 | socket: /var/run/mysqld/mysqld.sock 14 | timeout: 5000 15 | 16 | development: 17 | primary: 18 | <<: *default 19 | database: ottwatch_dev 20 | host: localhost 21 | v1: 22 | <<: *default 23 | database: ottwatch_dev_v1 24 | host: localhost 25 | 26 | # Warning: The database defined as "test" will be erased and 27 | # re-generated from your development database when you run "rake". 28 | # Do not set this db to the same as development or production. 29 | test: 30 | primary: 31 | <<: *default 32 | database: ottwatch_test 33 | host: localhost 34 | v1: 35 | <<: *default 36 | database: ottwatch_test_v1 37 | host: localhost 38 | 39 | production: 40 | primary: 41 | <<: *default 42 | database: <%= ENV["DB_NAME"] %> 43 | host: <%= ENV["DB_HOST"] %> 44 | username: <%= ENV["DB_USER"] %> 45 | password: <%= ENV["DB_PASS"] %> 46 | v1: 47 | <<: *default 48 | database: <%= ENV["DB_NAME_V1"] %> 49 | host: <%= ENV["DB_HOST"] %> 50 | username: <%= ENV["DB_USER"] %> 51 | password: <%= ENV["DB_PASS"] %> -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | 7 | ActionMailer::Base.smtp_settings = { 8 | :user_name => 'apikey', 9 | :password => ENV["SENDGRID_API_KEY"], 10 | :domain => 'ottwatch.ca', 11 | :address => 'smtp.sendgrid.net', 12 | :port => 587, 13 | :authentication => :plain, 14 | :enable_starttls_auto => true 15 | } -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # Code is not reloaded between requests. 7 | config.cache_classes = true 8 | 9 | # Eager load code on boot. This eager loads most of Rails and 10 | # your application in memory, allowing both threaded web servers 11 | # and those relying on copy on write to perform better. 12 | # Rake tasks automatically ignore this option for performance. 13 | config.eager_load = true 14 | 15 | # Full error reports are disabled and caching is turned on. 16 | config.consider_all_requests_local = false 17 | config.action_controller.perform_caching = true 18 | 19 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] 20 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). 21 | # config.require_master_key = true 22 | 23 | # Disable serving static files from the `/public` folder by default since 24 | # Apache or NGINX already handles this. 25 | config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present? 26 | 27 | # Compress CSS using a preprocessor. 28 | # config.assets.css_compressor = :sass 29 | 30 | # Do not fallback to assets pipeline if a precompiled asset is missed. 31 | config.assets.compile = false 32 | 33 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 34 | # config.asset_host = "http://assets.example.com" 35 | 36 | # Specifies the header that your server uses for sending files. 37 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache 38 | # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX 39 | 40 | # Store uploaded files on the local file system (see config/storage.yml for options). 41 | config.active_storage.service = :gcs 42 | 43 | # Mount Action Cable outside main process or domain. 44 | # config.action_cable.mount_path = nil 45 | # config.action_cable.url = "wss://example.com/cable" 46 | # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] 47 | 48 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 49 | # config.force_ssl = true 50 | 51 | # Include generic and useful information about system operation, but avoid logging too much 52 | # information to avoid inadvertent exposure of personally identifiable information (PII). 53 | config.log_level = :info 54 | 55 | # Prepend all log lines with the following tags. 56 | config.log_tags = [ :request_id ] 57 | 58 | # Use a different cache store in production. 59 | # config.cache_store = :mem_cache_store 60 | 61 | # Use a real queuing backend for Active Job (and separate queues per environment). 62 | config.active_job.queue_adapter = :solid_queue 63 | config.solid_queue.connects_to = { database: { writing: :production } } 64 | config.active_job.queue_name_prefix = "ottwatch_production" 65 | 66 | config.action_mailer.perform_caching = false 67 | 68 | # Ignore bad email addresses and do not raise email delivery errors. 69 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 70 | # config.action_mailer.raise_delivery_errors = false 71 | 72 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 73 | # the I18n.default_locale when a translation cannot be found). 74 | config.i18n.fallbacks = true 75 | 76 | # Don't log any deprecations. 77 | config.active_support.report_deprecations = false 78 | 79 | # Use default logging formatter so that PID and timestamp are not suppressed. 80 | config.log_formatter = ::Logger::Formatter.new 81 | 82 | # Use a different logger for distributed setups. 83 | # require "syslog/logger" 84 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name") 85 | 86 | if ENV["RAILS_LOG_TO_STDOUT"].present? 87 | logger = ActiveSupport::Logger.new(STDOUT) 88 | logger.formatter = config.log_formatter 89 | config.logger = ActiveSupport::TaggedLogging.new(logger) 90 | end 91 | 92 | # Do not dump schema after migrations. 93 | config.active_record.dump_schema_after_migration = false 94 | 95 | config.action_mailer.default_url_options = { host: 'ottwatch.ca', port: 443 } 96 | end 97 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | # The test environment is used exclusively to run your application's 4 | # test suite. You never need to work with it otherwise. Remember that 5 | # your test database is "scratch space" for the test suite and is wiped 6 | # and recreated between test runs. Don't rely on the data there! 7 | 8 | Rails.application.configure do 9 | config.after_initialize do 10 | Bullet.enable = true 11 | Bullet.bullet_logger = true 12 | Bullet.raise = true # raise an error if n+1 query occurs 13 | end 14 | 15 | # Settings specified here will take precedence over those in config/application.rb. 16 | 17 | # Turn false under Spring and add config.action_view.cache_template_loading = true 18 | config.cache_classes = false 19 | 20 | # Eager loading loads your whole application. When running a single test locally, 21 | # this probably isn't necessary. It's a good idea to do in a continuous integration 22 | # system, or in some way before deploying your code. 23 | config.eager_load = ENV["CI"].present? 24 | 25 | # Configure public file server for tests with Cache-Control for performance. 26 | config.public_file_server.enabled = true 27 | config.public_file_server.headers = { 28 | "Cache-Control" => "public, max-age=#{1.hour.to_i}" 29 | } 30 | 31 | # Show full error reports and disable caching. 32 | config.consider_all_requests_local = true 33 | config.action_controller.perform_caching = false 34 | config.cache_store = :null_store 35 | 36 | # Raise exceptions instead of rendering exception templates. 37 | config.action_dispatch.show_exceptions = false 38 | 39 | # Disable request forgery protection in test environment. 40 | config.action_controller.allow_forgery_protection = false 41 | 42 | # Store uploaded files on the local file system in a temporary directory. 43 | config.active_storage.service = :test 44 | 45 | config.action_mailer.perform_caching = false 46 | 47 | # Tell Action Mailer not to deliver emails to the real world. 48 | # The :test delivery method accumulates sent emails in the 49 | # ActionMailer::Base.deliveries array. 50 | config.action_mailer.delivery_method = :test 51 | 52 | # Print deprecation notices to the stderr. 53 | config.active_support.deprecation = :stderr 54 | 55 | # Raise exceptions for disallowed deprecations. 56 | config.active_support.disallowed_deprecation = :raise 57 | 58 | # Tell Active Support which deprecation messages to disallow. 59 | config.active_support.disallowed_deprecation_warnings = [] 60 | 61 | # for re-recording VCR casessettes faster (i/o network bound) 62 | # config.active_support.test_parallelization_threshold = 10 63 | 64 | # Raises error for missing translations. 65 | # config.i18n.raise_on_missing_translations = true 66 | 67 | # Annotate rendered view with file names. 68 | # config.action_view.annotate_rendered_view_with_filenames = true 69 | 70 | config.active_job.queue_adapter = :test 71 | end 72 | -------------------------------------------------------------------------------- /config/importmap.rb: -------------------------------------------------------------------------------- 1 | # Pin npm packages by running ./bin/importmap 2 | 3 | pin "application", preload: true 4 | pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true 5 | pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true 6 | pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true 7 | pin_all_from "app/javascript/controllers", under: "controllers" 8 | pin "maplibre-gl", to: "https://ga.jspm.io/npm:maplibre-gl@3.3.1/dist/maplibre-gl.js" 9 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = "1.0" 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in the app/assets 11 | # folder are already added. 12 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 13 | -------------------------------------------------------------------------------- /config/initializers/bugsnag.rb: -------------------------------------------------------------------------------- 1 | Bugsnag.configure do |config| 2 | if Rails.env.production? 3 | config.api_key = ENV.fetch("BUGSNAG_KEY") 4 | config.app_version = JSON.parse(File.read("version.json"))["object"]["sha"] 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy 4 | # For further information see the following documentation 5 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 6 | 7 | # Rails.application.configure do 8 | # config.content_security_policy do |policy| 9 | # policy.default_src :self, :https 10 | # policy.font_src :self, :https, :data 11 | # policy.img_src :self, :https, :data 12 | # policy.object_src :none 13 | # policy.script_src :self, :https 14 | # policy.style_src :self, :https 15 | # # Specify URI for violation reports 16 | # # policy.report_uri "/csp-violation-report-endpoint" 17 | # end 18 | # 19 | # # Generate session nonces for permitted importmap and inline scripts 20 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } 21 | # config.content_security_policy_nonce_directives = %w(script-src) 22 | # 23 | # # Report CSP violations to a specified URI. See: 24 | # # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only 25 | # # config.content_security_policy_report_only = true 26 | # end 27 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [ 5 | :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn 6 | ] 7 | -------------------------------------------------------------------------------- /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/permissions_policy.rb: -------------------------------------------------------------------------------- 1 | # Define an application-wide HTTP permissions policy. For further 2 | # information see https://developers.google.com/web/updates/2018/06/feature-policy 3 | # 4 | # Rails.application.config.permissions_policy do |f| 5 | # f.camera :none 6 | # f.gyroscope :none 7 | # f.microphone :none 8 | # f.usb :none 9 | # f.fullscreen :self 10 | # f.payment :self, "https://secure.example.com" 11 | # end 12 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t "hello" 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t("hello") %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # "true": "foo" 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at https://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers: a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum; this matches the default thread size of Active Record. 6 | # 7 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 8 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } 9 | threads min_threads_count, max_threads_count 10 | 11 | # Specifies the `worker_timeout` threshold that Puma will use to wait before 12 | # terminating a worker in development environments. 13 | # 14 | worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" 15 | 16 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 17 | # 18 | # port ENV.fetch("PORT") { 3000 } 19 | 20 | bind 'tcp://0.0.0.0:3000' 21 | 22 | # Specifies the `environment` that Puma will run in. 23 | # 24 | environment ENV.fetch("RAILS_ENV") { "development" } 25 | 26 | # Specifies the `pidfile` that Puma will use. 27 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } 28 | 29 | # Specifies the number of `workers` to boot in clustered mode. 30 | # Workers are forked web server processes. If using threads and workers together 31 | # the concurrency of the application would be max `threads` * `workers`. 32 | # Workers do not work on JRuby or Windows (both of which do not support 33 | # processes). 34 | # 35 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 36 | 37 | # Use the `preload_app!` method when specifying a `workers` number. 38 | # This directive tells Puma to first boot the application and load code 39 | # before forking the application. This takes advantage of Copy On Write 40 | # process behavior so workers use less memory. 41 | # 42 | # preload_app! 43 | 44 | # Allow puma to be restarted by `bin/rails restart` command. 45 | plugin :tmp_restart 46 | plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] 47 | 48 | -------------------------------------------------------------------------------- /config/queue.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | dispatchers: 3 | - polling_interval: 1 4 | batch_size: 500 5 | workers: 6 | - queues: "*" 7 | threads: 3 8 | processes: <%= ENV.fetch("JOB_CONCURRENCY", 1) %> 9 | polling_interval: 0.1 10 | 11 | development: 12 | <<: *default 13 | 14 | test: 15 | <<: *default 16 | 17 | production: 18 | <<: *default 19 | -------------------------------------------------------------------------------- /config/recurring.yml: -------------------------------------------------------------------------------- 1 | production: 2 | dev_app_scan_job: 3 | class: DevAppScanJob 4 | schedule: "30 * * * *" 5 | lobbying_scan_job: 6 | class: LobbyingScanJob 7 | schedule: "45 10-20 * * *" 8 | meeting_scan_job: 9 | class: MeetingScanJob 10 | schedule: "0 * * * *" 11 | parcel_scanner: 12 | class: ParcelScanner 13 | schedule: "0 2 1,2,3 * *" 14 | zoning_scanner: 15 | class: ZoningScanner 16 | schedule: "0 2 1,2,3 * *" 17 | consultation_scanner: 18 | class: ConsultationScanner 19 | schedule: "10 * * * *" 20 | traffic_camera_scrape_job: 21 | class: TrafficCameraScrapeJob 22 | schedule: "*/5 * * * *" 23 | syndication_job: 24 | class: SyndicationJob 25 | schedule: "* * * * *" -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | get 'announcement/index' 3 | get 'transpo/show_stop' 4 | 5 | devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' } 6 | 7 | get 'home/index' 8 | root "home#index" 9 | 10 | get 'traffic_cameras', to: 'traffic_cameras#index' 11 | get 'traffic_cameras/:id', to: 'traffic_cameras#show', as: 'traffic_camera' 12 | get 'traffic_cameras/:id/capture', to: 'traffic_cameras#capture', as: 'traffic_camera_capture' 13 | 14 | get 'parcels/:id', to: 'parcels#show' 15 | 16 | get 'devapp/map', to: 'devapp#map' 17 | get 'devapp/map_data', to: 'devapp#map_data' 18 | get 'devapp/index' 19 | get 'devapp/:app_number', to: 'devapp#show', as: 'devapp' 20 | 21 | get 'lobbying/index' 22 | get 'lobbying/:id', to: 'lobbying#show' 23 | 24 | get 'meeting/index' 25 | get 'meeting/:reference_id', to: 'meeting#show' 26 | 27 | get 'team/index' 28 | end 29 | -------------------------------------------------------------------------------- /config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | gcs: 10 | service: GCS 11 | project: ottwatch 12 | credentials: <%= ENV["GCS_KEYFILE"] %> 13 | bucket: ottwatch_<%= Rails.env %> 14 | 15 | # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 16 | # amazon: 17 | # service: S3 18 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 19 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 20 | # region: us-east-1 21 | # bucket: your_own_bucket-<%= Rails.env %> 22 | 23 | # Remember not to checkin your GCS keyfile to a repository 24 | # google: 25 | # service: GCS 26 | # project: your_project 27 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 28 | # bucket: your_own_bucket-<%= Rails.env %> 29 | 30 | # Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 31 | # microsoft: 32 | # service: AzureStorage 33 | # storage_account_name: your_account_name 34 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 35 | # container: your_container_name-<%= Rails.env %> 36 | 37 | # mirror: 38 | # service: Mirror 39 | # primary: local 40 | # mirrors: [ amazon, google, microsoft ] 41 | -------------------------------------------------------------------------------- /db/cable_schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema[7.1].define(version: 1) do 2 | create_table "solid_cable_messages", force: :cascade do |t| 3 | t.binary "channel", limit: 1024, null: false 4 | t.binary "payload", limit: 536870912, null: false 5 | t.datetime "created_at", null: false 6 | t.integer "channel_hash", limit: 8, null: false 7 | t.index ["channel"], name: "index_solid_cable_messages_on_channel" 8 | t.index ["channel_hash"], name: "index_solid_cable_messages_on_channel_hash" 9 | t.index ["created_at"], name: "index_solid_cable_messages_on_created_at" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20220122130113_create_active_storage_tables.active_storage.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from active_storage (originally 20170806125915) 2 | class CreateActiveStorageTables < ActiveRecord::Migration[5.2] 3 | def change 4 | # Use Active Record's configured type for primary and foreign keys 5 | primary_key_type, foreign_key_type = primary_and_foreign_key_types 6 | 7 | create_table :active_storage_blobs, id: primary_key_type do |t| 8 | t.string :key, null: false 9 | t.string :filename, null: false 10 | t.string :content_type 11 | t.text :metadata 12 | t.string :service_name, null: false 13 | t.bigint :byte_size, null: false 14 | t.string :checksum 15 | 16 | if connection.supports_datetime_with_precision? 17 | t.datetime :created_at, precision: 6, null: false 18 | else 19 | t.datetime :created_at, null: false 20 | end 21 | 22 | t.index [ :key ], unique: true 23 | end 24 | 25 | create_table :active_storage_attachments, id: primary_key_type do |t| 26 | t.string :name, null: false 27 | t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type 28 | t.references :blob, null: false, type: foreign_key_type 29 | 30 | if connection.supports_datetime_with_precision? 31 | t.datetime :created_at, precision: 6, null: false 32 | else 33 | t.datetime :created_at, null: false 34 | end 35 | 36 | t.index [ :record_type, :record_id, :name, :blob_id ], name: :index_active_storage_attachments_uniqueness, unique: true 37 | t.foreign_key :active_storage_blobs, column: :blob_id 38 | end 39 | 40 | create_table :active_storage_variant_records, id: primary_key_type do |t| 41 | t.belongs_to :blob, null: false, index: false, type: foreign_key_type 42 | t.string :variation_digest, null: false 43 | 44 | t.index [ :blob_id, :variation_digest ], name: :index_active_storage_variant_records_uniqueness, unique: true 45 | t.foreign_key :active_storage_blobs, column: :blob_id 46 | end 47 | end 48 | 49 | private 50 | def primary_and_foreign_key_types 51 | config = Rails.configuration.generators 52 | setting = config.options[config.orm][:primary_key_type] 53 | primary_key_type = setting || :primary_key 54 | foreign_key_type = setting || :bigint 55 | [primary_key_type, foreign_key_type] 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /db/migrate/20220207012208_create_dev_app_entries.rb: -------------------------------------------------------------------------------- 1 | class CreateDevAppEntries < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :dev_app_entries do |t| 4 | t.string :app_id 5 | t.string :app_number 6 | t.string :app_type 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20220208170430_create_dev_app_addresses.rb: -------------------------------------------------------------------------------- 1 | class CreateDevAppAddresses < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :dev_app_addresses do |t| 4 | t.references :entry, class_name: "DevApp::Entry" 5 | t.string :ref_id 6 | t.string :road_number 7 | t.string :qualifier 8 | t.string :legal_unit 9 | t.string :road_name 10 | t.string :direction 11 | t.string :road_type 12 | t.string :municipality 13 | t.string :address_type 14 | t.decimal :lat, precision: 15, scale: 10 15 | t.decimal :lon, precision: 15, scale: 10 16 | t.string :parcel_pin 17 | 18 | t.timestamps 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /db/migrate/20220209221445_create_dev_app_documents.rb: -------------------------------------------------------------------------------- 1 | class CreateDevAppDocuments < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :dev_app_documents do |t| 4 | t.references :entry, null: false, foreign_key: true, class_name: "DevApp::Entry", foreign_key: {to_table: :dev_app_entries} 5 | t.string :ref_id 6 | t.string :name 7 | t.string :path 8 | t.string :url 9 | 10 | t.timestamps 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20220211193125_add_desc_to_dev_app_entry.rb: -------------------------------------------------------------------------------- 1 | class AddDescToDevAppEntry < ActiveRecord::Migration[7.0] 2 | def change 3 | add_column(:dev_app_entries, :desc, :string) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20220211193615_create_dev_app_statuses.rb: -------------------------------------------------------------------------------- 1 | class CreateDevAppStatuses < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :dev_app_statuses do |t| 4 | t.references :entry, null: false, foreign_key: true, class_name: "DevApp::Entry", foreign_key: {to_table: :dev_app_entries} 5 | t.string :status 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20220212131300_create_announcements.rb: -------------------------------------------------------------------------------- 1 | class CreateAnnouncements < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :announcements do |t| 4 | t.string :message 5 | t.bigint :reference_id 6 | t.string :reference_type 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20220221183941_create_parcels.rb: -------------------------------------------------------------------------------- 1 | class CreateParcels < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :parcels do |t| 4 | t.integer :objectid 5 | t.string :pin 6 | t.decimal :easting, precision: 15, scale: 3 7 | t.decimal :northing, precision: 15, scale: 3 8 | t.string :publicland 9 | t.string :parceltype 10 | t.string :pi_municipal_address_id 11 | t.string :record_owner_id 12 | t.string :rt_road_name_id 13 | t.string :address_number 14 | t.string :road_name 15 | t.string :suffix 16 | t.string :dir 17 | t.string :municipality_name 18 | t.string :legal_unit 19 | t.string :address_qualifier 20 | t.string :postal_code 21 | t.string :address_status 22 | t.string :address_type_id 23 | t.string :pin_number 24 | t.integer :feat_num 25 | t.string :pi_parcel_id 26 | t.decimal :shape_length, precision: 25, scale: 15 27 | t.decimal :shape_area, precision: 25, scale: 15 28 | t.timestamps 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /db/migrate/20220402214403_dev_app_entry_description_wider.rb: -------------------------------------------------------------------------------- 1 | class DevAppEntryDescriptionWider < ActiveRecord::Migration[7.0] 2 | def change 3 | change_column :dev_app_entries, :desc, :text 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20220403190017_create_global_controls.rb: -------------------------------------------------------------------------------- 1 | class CreateGlobalControls < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :global_controls do |t| 4 | t.string :name, index: { unique: true } 5 | t.string :value 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20220409210514_add_state_to_dev_app_documents.rb: -------------------------------------------------------------------------------- 1 | class AddStateToDevAppDocuments < ActiveRecord::Migration[7.0] 2 | def change 3 | add_column :dev_app_documents, :state, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20220410023931_add_planner_to_dev_app_entry.rb: -------------------------------------------------------------------------------- 1 | class AddPlannerToDevAppEntry < ActiveRecord::Migration[7.0] 2 | def change 3 | add_column :dev_app_entries, :planner_first_name, :string 4 | add_column :dev_app_entries, :planner_last_name, :string 5 | add_column :dev_app_entries, :planner_phone, :string 6 | add_column :dev_app_entries, :planner_email, :string 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20220416031244_create_committees.rb: -------------------------------------------------------------------------------- 1 | class CreateCommittees < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :committees do |t| 4 | t.string :name 5 | 6 | t.timestamps 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20220416031533_create_meetings.rb: -------------------------------------------------------------------------------- 1 | class CreateMeetings < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :meetings do |t| 4 | t.references :committee, null: false, foreign_key: true 5 | t.time :start_time 6 | t.string :contact_name 7 | t.string :contact_email 8 | t.string :contact_phone 9 | t.integer :reference_id 10 | 11 | t.timestamps 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/20220416031611_create_meeting_items.rb: -------------------------------------------------------------------------------- 1 | class CreateMeetingItems < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :meeting_items do |t| 4 | t.string :title 5 | t.integer :reference_id 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20220416042402_change_meeting_start_time.rb: -------------------------------------------------------------------------------- 1 | class ChangeMeetingStartTime < ActiveRecord::Migration[7.0] 2 | def change 3 | change_table :meetings do |t| 4 | t.change :start_time, :datetime 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20220416183233_add_meeting_to_meeting_items.rb: -------------------------------------------------------------------------------- 1 | class AddMeetingToMeetingItems < ActiveRecord::Migration[7.0] 2 | def change 3 | add_reference :meeting_items, :meeting, null: false, foreign_key: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20220425003142_create_lobbying_undertakings.rb: -------------------------------------------------------------------------------- 1 | class CreateLobbyingUndertakings < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :lobbying_undertakings do |t| 4 | t.string :subject 5 | t.text :issue 6 | t.string :lobbyist_name 7 | t.string :lobbyist_position 8 | t.string :lobbyist_reg_type 9 | t.text :view_details 10 | 11 | t.timestamps 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/20220425004321_create_lobbying_activities.rb: -------------------------------------------------------------------------------- 1 | class CreateLobbyingActivities < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :lobbying_activities do |t| 4 | t.references :lobbying_undertaking, null: false, foreign_key: true 5 | t.date :activity_date 6 | t.string :activity_type 7 | t.string :lobbied_name 8 | t.string :lobbied_title 9 | 10 | t.timestamps 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20220502212849_create_elections.rb: -------------------------------------------------------------------------------- 1 | class CreateElections < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :elections do |t| 4 | t.date :date 5 | 6 | t.timestamps 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20220502213209_create_candidates.rb: -------------------------------------------------------------------------------- 1 | class CreateCandidates < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :candidates do |t| 4 | t.references :election, null: false, foreign_key: true 5 | t.integer :ward 6 | t.string :name 7 | t.date :nomination_date 8 | t.string :telephone 9 | t.string :email 10 | t.string :website 11 | 12 | t.timestamps 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20220522030307_add_geometry_to_parcels.rb: -------------------------------------------------------------------------------- 1 | class AddGeometryToParcels < ActiveRecord::Migration[7.0] 2 | def change 3 | add_column :parcels, :geometry_json, :text 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20220522031853_change_geometry_json_to_medium_text.rb: -------------------------------------------------------------------------------- 1 | class ChangeGeometryJsonToMediumText < ActiveRecord::Migration[7.0] 2 | def change 3 | change_column :parcels, :geometry_json, :mediumtext 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20220527125853_add_unique_index_to_parcels.rb: -------------------------------------------------------------------------------- 1 | class AddUniqueIndexToParcels < ActiveRecord::Migration[7.0] 2 | def change 3 | add_index :parcels, :objectid, :unique => true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20220805155127_add_reference_guid_to_meetings.rb: -------------------------------------------------------------------------------- 1 | class AddReferenceGuidToMeetings < ActiveRecord::Migration[7.0] 2 | def change 3 | add_column :meetings, :reference_guid, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20220821234844_add_withdrew_to_candidates.rb: -------------------------------------------------------------------------------- 1 | class AddWithdrewToCandidates < ActiveRecord::Migration[7.0] 2 | def change 3 | add_column :candidates, :withdrew, :boolean 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20220915011507_devise_create_users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class DeviseCreateUsers < ActiveRecord::Migration[7.0] 4 | def change 5 | create_table :users do |t| 6 | ## Database authenticatable 7 | t.string :email, null: false, default: "" 8 | t.string :encrypted_password, null: false, default: "" 9 | 10 | ## Recoverable 11 | t.string :reset_password_token 12 | t.datetime :reset_password_sent_at 13 | 14 | ## Rememberable 15 | t.datetime :remember_created_at 16 | 17 | ## Trackable 18 | t.integer :sign_in_count, default: 0, null: false 19 | t.datetime :current_sign_in_at 20 | t.datetime :last_sign_in_at 21 | t.string :current_sign_in_ip 22 | t.string :last_sign_in_ip 23 | 24 | ## Confirmable 25 | # t.string :confirmation_token 26 | # t.datetime :confirmed_at 27 | # t.datetime :confirmation_sent_at 28 | # t.string :unconfirmed_email # Only if using reconfirmable 29 | 30 | ## Lockable 31 | # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts 32 | # t.string :unlock_token # Only if unlock strategy is :email or :both 33 | # t.datetime :locked_at 34 | 35 | 36 | t.timestamps null: false 37 | end 38 | 39 | add_index :users, :email, unique: true 40 | add_index :users, :reset_password_token, unique: true 41 | # add_index :users, :confirmation_token, unique: true 42 | # add_index :users, :unlock_token, unique: true 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /db/migrate/20221018215417_add_provider_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddProviderToUsers < ActiveRecord::Migration[7.0] 2 | def change 3 | add_column :users, :provider, :string 4 | add_column :users, :uid, :string 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20221018215842_add_name_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddNameToUsers < ActiveRecord::Migration[7.0] 2 | def change 3 | add_column :users, :name, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20221018220043_add_username_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddUsernameToUsers < ActiveRecord::Migration[7.0] 2 | def change 3 | add_column :users, :username, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20221018220420_change_users_table.rb: -------------------------------------------------------------------------------- 1 | class ChangeUsersTable < ActiveRecord::Migration[7.0] 2 | def change 3 | change_column :users, :email, :string, null: true 4 | remove_index :users, [:email] # , name => "index_completions_on_survey_id_and_user_id" 5 | 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20221019023256_create_consultations.rb: -------------------------------------------------------------------------------- 1 | class CreateConsultations < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :consultations do |t| 4 | t.string :title 5 | t.string :href 6 | t.string :status 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20221107140208_create_service_requests.rb: -------------------------------------------------------------------------------- 1 | class CreateServiceRequests < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :service_requests do |t| 4 | t.string :service_request_id 5 | t.string :status 6 | t.string :status_notes 7 | t.string :service_name 8 | t.string :service_code 9 | t.string :description 10 | t.string :agency_responsible 11 | t.string :service_notice 12 | t.time :requested_datetime 13 | t.time :updated_datetime 14 | t.time :expected_datetime 15 | t.string :address 16 | t.string :address_id 17 | t.string :zipcode 18 | t.decimal :lat, precision: 15, scale: 10 19 | t.decimal :lon, precision: 15, scale: 10 20 | t.string :media_url 21 | 22 | t.timestamps 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /db/migrate/20221108022353_change_service_requests_times_to_dates.rb: -------------------------------------------------------------------------------- 1 | class ChangeServiceRequestsTimesToDates < ActiveRecord::Migration[7.0] 2 | def change 3 | change_table :service_requests do |t| 4 | t.change :requested_datetime, :datetime 5 | t.change :updated_datetime, :datetime 6 | t.change :expected_datetime, :datetime 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20221115154752_create_zonings.rb: -------------------------------------------------------------------------------- 1 | class CreateZonings < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :zonings do |t| 4 | t.integer :objectid 5 | t.decimal :shape_area, precision: 25, scale: 15 6 | t.decimal :shape_length, precision: 25, scale: 15 7 | t.string :bylaw_num 8 | t.string :cons_date 9 | t.string :cons_datef 10 | t.string :fp_group 11 | t.string :height 12 | t.string :heightinfo 13 | t.string :history 14 | t.string :label 15 | t.string :label_en 16 | t.string :label_fr 17 | t.string :link_en 18 | t.string :link_fr 19 | t.string :parentzone 20 | t.string :subtype 21 | t.string :url 22 | t.string :village_op 23 | t.string :zone_code 24 | t.string :zone_main 25 | t.string :zoningtype 26 | t.longtext :geometry_json 27 | 28 | t.timestamps 29 | 30 | t.index :objectid, unique: true 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /db/migrate/20230206010757_create_campaign_returns.rb: -------------------------------------------------------------------------------- 1 | class CreateCampaignReturns < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :campaign_returns do |t| 4 | t.references :candidate, null: false, foreign_key: true 5 | 6 | t.timestamps 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20230206010904_create_campaign_return_pages.rb: -------------------------------------------------------------------------------- 1 | class CreateCampaignReturnPages < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :campaign_return_pages do |t| 4 | t.references :campaign_return, null: false, foreign_key: true 5 | 6 | t.timestamps 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20230227025209_add_rotation_to_campaign_return_pages.rb: -------------------------------------------------------------------------------- 1 | class AddRotationToCampaignReturnPages < ActiveRecord::Migration[7.0] 2 | def change 3 | add_column :campaign_return_pages, :rotation, :integer, default: 0 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20230227133754_drop_campaign_donation.rb: -------------------------------------------------------------------------------- 1 | class DropCampaignDonation < ActiveRecord::Migration[7.0] 2 | def change 3 | drop_table :campaign_donations 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20230227134154_create_campaign_donations.rb: -------------------------------------------------------------------------------- 1 | class CreateCampaignDonations < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :campaign_donations do |t| 4 | t.references :campaign_return_page, null: false, foreign_key: true 5 | t.string :name 6 | t.string :address 7 | t.string :city 8 | t.string :prov 9 | t.string :postal 10 | t.decimal :amount, precision: 10, scale: 2 11 | t.decimal :x, precision: 10, scale: 4 12 | t.decimal :y, precision: 10, scale: 4 13 | t.date :donated_on 14 | t.boolean :redacted 15 | 16 | t.timestamps 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /db/migrate/20230403222136_add_url_to_campaign_returns.rb: -------------------------------------------------------------------------------- 1 | class AddUrlToCampaignReturns < ActiveRecord::Migration[7.0] 2 | def change 3 | add_column :campaign_returns, :url, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20240107220707_create_meeting_item_documents.rb: -------------------------------------------------------------------------------- 1 | class CreateMeetingItemDocuments < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :meeting_item_documents do |t| 4 | t.string :reference_id 5 | t.string :title 6 | t.references :meeting_item, null: false, foreign_key: true 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20240108032419_add_content_to_meeting_items.rb: -------------------------------------------------------------------------------- 1 | class AddContentToMeetingItems < ActiveRecord::Migration[7.0] 2 | def change 3 | add_column :meeting_items, :content, :text 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20240123032506_alter_title_on_meeting_items.rb: -------------------------------------------------------------------------------- 1 | class AlterTitleOnMeetingItems < ActiveRecord::Migration[7.0] 2 | def up 3 | change_table :meeting_items do |t| 4 | t.change :title, :text 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20240128024632_add_snapshot_date_to_parcels.rb: -------------------------------------------------------------------------------- 1 | class AddSnapshotDateToParcels < ActiveRecord::Migration[7.0] 2 | def change 3 | add_column :parcels, :snapshot_date, :date 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20240128025544_drop_object_id_index_from_parcels.rb: -------------------------------------------------------------------------------- 1 | class DropObjectIdIndexFromParcels < ActiveRecord::Migration[7.0] 2 | def change 3 | remove_index :parcels, name: "index_parcels_on_objectid" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20240128030031_add_scan_object_id_index_to_parcels.rb: -------------------------------------------------------------------------------- 1 | class AddScanObjectIdIndexToParcels < ActiveRecord::Migration[7.0] 2 | def change 3 | add_index :parcels, [:snapshot_date, :objectid], :unique => true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20240201195110_add_snapshot_date_to_zonings.rb: -------------------------------------------------------------------------------- 1 | class AddSnapshotDateToZonings < ActiveRecord::Migration[7.0] 2 | def change 3 | remove_index :zonings, name: "index_zonings_on_objectid" 4 | add_column :zonings, :snapshot_date, :date 5 | add_index :zonings, [:snapshot_date, :objectid], :unique => true 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20241006165929_create_traffic_cameras.rb: -------------------------------------------------------------------------------- 1 | class CreateTrafficCameras < ActiveRecord::Migration[7.1] 2 | def change 3 | create_table :traffic_cameras do |t| 4 | t.float :lat 5 | t.float :lon 6 | t.string :name 7 | t.string :camera_owner 8 | t.integer :camera_number 9 | t.string :reference_id 10 | 11 | t.timestamps 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/20250222190338_create_solid_cable_tables.rb: -------------------------------------------------------------------------------- 1 | class CreateSolidCableTables < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table "solid_cable_messages", force: :cascade do |t| 4 | t.binary "channel", limit: 1024, null: false 5 | t.binary "payload", limit: 536870912, null: false 6 | t.datetime "created_at", null: false 7 | t.integer "channel_hash", limit: 8, null: false 8 | t.index ["channel"], name: "index_solid_cable_messages_on_channel" 9 | t.index ["channel_hash"], name: "index_solid_cable_messages_on_channel_hash" 10 | t.index ["created_at"], name: "index_solid_cable_messages_on_created_at" 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20250304022406_add_client_details_to_lobbying_undertaking.rb: -------------------------------------------------------------------------------- 1 | class AddClientDetailsToLobbyingUndertaking < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :lobbying_undertakings, :client, :string 4 | add_column :lobbying_undertakings, :client_org, :string 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # movies = Movie.create([{ name: "Star Wars" }, { name: "Lord of the Rings" }]) 7 | # Character.create(name: "Luke", movie: movies.first) -------------------------------------------------------------------------------- /docker/Dockerfile.base: -------------------------------------------------------------------------------- 1 | # 2 | # docker build -t ottwatch-base -f Dockerfile.base . 3 | # docker run -v `pwd`:/app -i -t ottwatch-base 4 | # 5 | 6 | FROM ubuntu:latest 7 | 8 | RUN apt-get update && \ 9 | DEBIAN_FRONTEND=noninteractive apt-get -y install \ 10 | ruby-dev \ 11 | less \ 12 | curl \ 13 | vim \ 14 | sudo \ 15 | mysql-client \ 16 | mysql-server \ 17 | libmysqlclient-dev \ 18 | git \ 19 | rbenv \ 20 | poppler-utils \ 21 | imagemagick \ 22 | libmagick++-dev \ 23 | sqlite3 24 | 25 | RUN groupadd -g 2200 -r app && \ 26 | useradd -u 2200 --no-log-init -r -g app app && \ 27 | mkdir /home/app && \ 28 | chown app:app /home/app && \ 29 | adduser app sudo 30 | 31 | RUN gem install bundler 32 | -------------------------------------------------------------------------------- /docker/Dockerfile.dev: -------------------------------------------------------------------------------- 1 | # 2 | # Build the image 3 | # docker build -t ottwatch-dev -f Dockerfile.dev . 4 | # 5 | # Run the container and load the current directory at /ottwatch 6 | # docker run --name ottwatch-dev -p 33000:3000 -v `pwd`:/ottwatch -i -t ottwatch-dev 7 | # docker start -i ottwatch-dev 8 | # 9 | FROM ottwatch-base 10 | 11 | # bundle config set --local path 'vendor/bundle' 12 | # bundle install 13 | # bin/rails server -b 0.0.0.0 14 | # 15 | -------------------------------------------------------------------------------- /docker/Dockerfile.prod: -------------------------------------------------------------------------------- 1 | # 2 | # Build the image 3 | # sudo docker build -t ottwatch-prod -f Dockerfile.prod . 4 | # 5 | FROM ottwatch-base 6 | 7 | RUN mkdir /app && chown app:app /app 8 | WORKDIR /app 9 | USER app 10 | 11 | # clone early, bundle, to avoid always repeating main bundle install on each deploy 12 | RUN git clone https://github.com/kevinodotnet/ottwatch.git --single-branch 13 | WORKDIR /app/ottwatch 14 | RUN bundle config set --local path 'vendor/bundle' 15 | RUN bundle install 16 | 17 | # only once per commit to main 18 | RUN git checkout . && git clean -df . 19 | ADD --chown=app:app https://api.github.com/repos/kevinodotnet/ottwatch/git/refs/heads/main /tmp/version.json 20 | ADD --chown=app:app https://api.github.com/repos/kevinodotnet/ottwatch/git/refs/heads/main version.json 21 | RUN git pull 22 | RUN bundle install 23 | RUN bin/rails assets:precompile 24 | ENV RAILS_LOG_TO_STDOUT true 25 | -------------------------------------------------------------------------------- /docker/dev-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd `dirname $0` 4 | 5 | # --progress=plain 6 | # 7 | # BUILD_NO_CACHE=--no-cache ./dev-build.sh 8 | 9 | docker build $BUILD_NO_CACHE -t ottwatch-base -f Dockerfile.base . && \ 10 | docker build $BUILD_NO_CACHE -t ottwatch-dev -f Dockerfile.dev . 11 | -------------------------------------------------------------------------------- /docker/dev-exec.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | instance=$1 4 | if [ -z "${instance}" ]; then 5 | instance="ottwatch-dev" 6 | fi 7 | 8 | docker exec -it $instance /bin/bash 9 | 10 | -------------------------------------------------------------------------------- /docker/dev-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd `dirname $0` 4 | 5 | # create/run the container, or if it already exists, restart the existing one 6 | # use 'docker container rm ottwatch-dev' to reset from the beginning 7 | 8 | cat << EOF 9 | # 10 | # when container is run/re-started: 11 | # 12 | /etc/init.d/mysql start 13 | 14 | # 15 | # on first run of container: 16 | # 17 | cd ottwatch; bin/rails db:setup 18 | EOF 19 | 20 | instance=$1 21 | if [ -z "${instance}" ]; then 22 | instance="ottwatch-dev" 23 | fi 24 | 25 | port=$2 26 | if [ -z "${port}" ]; then 27 | port=33000 28 | fi 29 | 30 | docker run --name $instance -p $port:3000 -v `pwd`/..:/ottwatch -i -t ottwatch-dev || \ 31 | docker start -i $instance 32 | 33 | -------------------------------------------------------------------------------- /docker/pdev-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # --progress=plain 4 | # 5 | # BUILD_NO_CACHE=--no-cache ./dev-build.sh 6 | 7 | podman build $BUILD_NO_CACHE -t ottwatch-base -f Dockerfile.base . && \ 8 | podman build $BUILD_NO_CACHE -t ottwatch-dev -f Dockerfile.dev . 9 | -------------------------------------------------------------------------------- /docker/pdev-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # create/run the container, or if it already exists, restart the existing one 4 | # use 'docker container rm ottwatch-dev' to reset from the beginning 5 | 6 | cat << EOF 7 | # 8 | # when container is run/re-started: 9 | # 10 | /etc/init.d/mysql start 11 | 12 | # 13 | # on first run of container: 14 | # 15 | cd ottwatch; bin/rails db:setup 16 | EOF 17 | 18 | instance=$1 19 | if [ -z "${instance}" ]; then 20 | instance="ottwatch-dev" 21 | fi 22 | 23 | port=$2 24 | if [ -z "${port}" ]; then 25 | port=34000 26 | fi 27 | 28 | podman run --name $instance -p $port:3000 -v `pwd`/..:/ottwatch -i -t ottwatch-dev 29 | 30 | -------------------------------------------------------------------------------- /docker/prod-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . ~/src/infra/ottwatch-snack.sh 4 | 5 | sudo docker build $BUILD_NO_CACHE -t ottwatch-base -f Dockerfile.base . 6 | sudo docker build $BUILD_NO_CACHE -t ottwatch-prod -f Dockerfile.prod . 7 | sudo docker image prune -f 8 | -------------------------------------------------------------------------------- /docker/prod-deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # BUILD_NO_CACHE=--no-cache ./prod-deploy.sh 4 | 5 | cd `dirname $0` 6 | 7 | . ~/src/infra/ottwatch-snack.sh 8 | 9 | echo "############################" 10 | date 11 | echo "Building..." 12 | echo "" 13 | ./prod-build.sh 14 | 15 | echo "" 16 | echo "############################" 17 | date 18 | echo "Stopping..." 19 | echo "" 20 | ./prod-stop.sh 21 | 22 | echo "" 23 | echo "############################" 24 | date 25 | echo "Migrating..." 26 | echo "" 27 | ./prod-exec.sh bin/rails db:migrate:primary 28 | 29 | echo "" 30 | echo "############################" 31 | date 32 | echo "Web..." 33 | echo "" 34 | ./prod-web.sh 35 | 36 | echo "" 37 | echo "############################" 38 | date 39 | echo "Done..." 40 | echo "" 41 | -------------------------------------------------------------------------------- /docker/prod-exec.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . ~/src/infra/ottwatch-snack.sh 4 | 5 | sudo docker run \ 6 | --rm \ 7 | --network $DOCKER_NETWORK \ 8 | -e BLUE_SKY_PASSWORD=$BLUE_SKY_PASSWORD \ 9 | -e BUGSNAG_KEY=$BUGSNAG_KEY \ 10 | -e DB_HOST=$DB_HOST \ 11 | -e DB_NAME=$DB_NAME \ 12 | -e DB_NAME_V1=$DB_NAME_V1 \ 13 | -e DB_PASS=$DB_PASS \ 14 | -e DB_USER=$DB_USER \ 15 | -e MASTEDON_ACCESS_TOKEN=$MASTEDON_ACCESS_TOKEN \ 16 | -e RAILS_ENV=production \ 17 | -e RAILS_MASTER_KEY=$RAILS_MASTER_KEY \ 18 | -e GCS_KEYFILE=/infra/gcs-prodweb-service-account.json \ 19 | -e REDIS_URL=$REDIS_URL \ 20 | -e SENDGRID_API_KEY=$SENDGRID_PRODWEB_FULL \ 21 | -e LOCAL_STORAGE_FOLDER=$LOCAL_STORAGE_FOLDER_CLIENT \ 22 | -v $INFRA_FOLDER:/infra \ 23 | -v $LOCAL_STORAGE_FOLDER_HOST:$LOCAL_STORAGE_FOLDER_CLIENT \ 24 | -i -t \ 25 | ottwatch-prod $* 26 | 27 | -------------------------------------------------------------------------------- /docker/prod-stop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . ~/src/infra/ottwatch-snack.sh 4 | 5 | sudo docker stop ottwatch-web 6 | -------------------------------------------------------------------------------- /docker/prod-web.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . ~/src/infra/ottwatch-snack.sh 4 | 5 | sudo docker container stop ottwatch-web 6 | sudo docker container rm ottwatch-web 7 | 8 | sudo docker run \ 9 | --restart always \ 10 | -d \ 11 | --network $DOCKER_NETWORK \ 12 | --ip 10.50.1.13 \ 13 | -e BLUE_SKY_PASSWORD=$BLUE_SKY_PASSWORD \ 14 | -e BUGSNAG_KEY=$BUGSNAG_KEY \ 15 | -e DB_HOST=$DB_HOST \ 16 | -e DB_NAME=$DB_NAME \ 17 | -e DB_NAME_V1=$DB_NAME_V1 \ 18 | -e DB_PASS=$DB_PASS \ 19 | -e DB_USER=$DB_USER \ 20 | -e SOLID_QUEUE_IN_PUMA=1 \ 21 | -e GCS_KEYFILE=/infra/gcs-prodweb-service-account.json \ 22 | -e GOOGLE_MAPS_API_KEY=$GOOGLE_MAPS_API_KEY \ 23 | -e GOOGLE_WEB_APP_CLIENT_ID=$GOOGLE_WEB_APP_CLIENT_ID \ 24 | -e GOOGLE_WEB_APP_CLIENT_SECRET=$GOOGLE_WEB_APP_CLIENT_SECRET \ 25 | -e RAILS_ENV=production \ 26 | -e RAILS_MASTER_KEY=$RAILS_MASTER_KEY \ 27 | -e RAILS_SERVE_STATIC_FILES=1 \ 28 | -e REDIS_URL=$REDIS_URL \ 29 | -e SENDGRID_API_KEY=$SENDGRID_PRODWEB_FULL \ 30 | -e OCTRANSPO_APP_ID=$OCTRANSPO_APP_ID \ 31 | -e OCTRANSPO_APP_KEY=$OCTRANSPO_APP_KEY \ 32 | -e RAILS_MAX_THREADS=10 \ 33 | -e GITHUB_APP_ID=$GITHUB_APP_ID \ 34 | -e GITHUB_APP_SECRET=$GITHUB_APP_SECRET \ 35 | -e LOCAL_STORAGE_FOLDER=$LOCAL_STORAGE_FOLDER_CLIENT \ 36 | -v $INFRA_FOLDER:/infra \ 37 | -v $LOCAL_STORAGE_FOLDER_HOST:$LOCAL_STORAGE_FOLDER_CLIENT \ 38 | -p 3000:3000 \ 39 | --name ottwatch-web \ 40 | ottwatch-prod bin/rails server 41 | 42 | -------------------------------------------------------------------------------- /fixtures/four_pages.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinodotnet/ottwatch/2e337b8583bb932234052e6c6a3a4aac516bcd5a/fixtures/four_pages.pdf -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/DevApp_ScannerTest_test_issue_102_regression_fix.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://devapps-restapi.ottawa.ca/devapps/search?appStatus=all&appType=all&authKey=4r5T2egSmKm5&bounds=0,0,0,0&searchText=D07-12-15-0165&ward=all 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Accept-Encoding: 11 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 12 | Accept: 13 | - "*/*" 14 | User-Agent: 15 | - Ruby 16 | Host: 17 | - devapps-restapi.ottawa.ca 18 | response: 19 | status: 20 | code: 200 21 | message: OK 22 | headers: 23 | Server: 24 | - nginx 25 | Date: 26 | - Sat, 22 Feb 2025 17:42:20 GMT 27 | Content-Type: 28 | - application/json; charset=utf-8 29 | Transfer-Encoding: 30 | - chunked 31 | Strict-Transport-Security: 32 | - max-age=15724800; includeSubDomains 33 | X-Xss-Protection: 34 | - 1; mode=block 35 | X-Frame-Options: 36 | - SAMEORIGIN 37 | X-Ott-Cache: 38 | - '27' 39 | Set-Cookie: 40 | - incap_ses_1710_2348304=eSkmQZX2qW2tTqMzLSS7F3wMumcAAAAA4CPRdTyr0dtIIb98UJ6GSA==; 41 | path=/; Domain=.ottawa.ca 42 | - nlbi_2348304=LQopZYaPDj7iWK+JuLiUiQAAAABknz74UAE0vnnKzNkReDq8; HttpOnly; path=/; 43 | Domain=.ottawa.ca 44 | - visid_incap_2348304=xaAIlCPnTEOWO+Njl5J2cXwMumcAAAAAQUIPAAAAAAAnFhCMZ2yZSmV1RarLpWuh; 45 | expires=Sun, 22 Feb 2026 06:56:40 GMT; HttpOnly; path=/; Domain=.ottawa.ca 46 | X-Cdn: 47 | - Imperva 48 | X-Iinfo: 49 | - 52-5128015-5127979 PNYy RT(1740246139960 9) q(0 0 0 -1) r(1 1) U24 50 | body: 51 | encoding: ASCII-8BIT 52 | string: '{"devApps":[],"totalDevApps":0,"index":0,"limit":0}' 53 | recorded_at: Sat, 22 Feb 2025 17:42:20 GMT 54 | recorded_with: VCR 6.3.1 55 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/DevApp_ScannerTest_test_issue_102_regression_fix_02.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://devapps-restapi.ottawa.ca/devapps/search?appStatus=all&appType=all&authKey=4r5T2egSmKm5&bounds=0,0,0,0&searchText=D01-01-18-0001&ward=all 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Accept-Encoding: 11 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 12 | Accept: 13 | - "*/*" 14 | User-Agent: 15 | - Ruby 16 | Host: 17 | - devapps-restapi.ottawa.ca 18 | response: 19 | status: 20 | code: 200 21 | message: OK 22 | headers: 23 | Server: 24 | - nginx 25 | Date: 26 | - Sat, 22 Feb 2025 17:42:21 GMT 27 | Content-Type: 28 | - application/json; charset=utf-8 29 | Transfer-Encoding: 30 | - chunked 31 | Strict-Transport-Security: 32 | - max-age=15724800; includeSubDomains 33 | X-Xss-Protection: 34 | - 1; mode=block 35 | X-Frame-Options: 36 | - SAMEORIGIN 37 | X-Ott-Cache: 38 | - '28' 39 | Set-Cookie: 40 | - incap_ses_1710_2348304=qo3LZwkfSCA4T6MzLSS7F3wMumcAAAAAbiSd/7ExcuYA4twT/uQwTg==; 41 | path=/; Domain=.ottawa.ca 42 | - nlbi_2348304=+IXwfYV6mRCSGIXxuLiUiQAAAAAcvhCBmvwGfImSaeq72CPv; HttpOnly; path=/; 43 | Domain=.ottawa.ca 44 | - visid_incap_2348304=Wjooo8MbTweesClUm/qEQ3wMumcAAAAAQUIPAAAAAADbmR0yrdE/GVUn7D6mAczh; 45 | expires=Sun, 22 Feb 2026 06:56:35 GMT; HttpOnly; path=/; Domain=.ottawa.ca 46 | X-Cdn: 47 | - Imperva 48 | X-Iinfo: 49 | - 62-43687619-43684689 PNYN RT(1740246140831 9) q(0 0 0 -1) r(1 1) U24 50 | body: 51 | encoding: ASCII-8BIT 52 | string: '{"devApps":[],"totalDevApps":0,"index":0,"limit":0}' 53 | recorded_at: Sat, 22 Feb 2025 17:42:21 GMT 54 | recorded_with: VCR 6.3.1 55 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinodotnet/ottwatch/2e337b8583bb932234052e6c6a3a4aac516bcd5a/lib/assets/.keep -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinodotnet/ottwatch/2e337b8583bb932234052e6c6a3a4aac516bcd5a/lib/tasks/.keep -------------------------------------------------------------------------------- /lib/tasks/ottwatch.rake: -------------------------------------------------------------------------------- 1 | require 'csv' 2 | 3 | namespace :ottwatch do 4 | desc "Seed data" 5 | task seed: :environment do 6 | 7 | Nokogiri::HTML(Net::HTTP.get(URI("https://pub-ottawa.escribemeetings.com/"))).xpath('//div[@class="calendar-item"]').each do |m| 8 | md = Nokogiri::HTML(m.to_s) 9 | 10 | title = md.xpath('//div[@class="meeting-title"]/h3/span').children.to_s 11 | meeting_time = md.xpath('//div[@class="meeting-date"]').first.children.to_s 12 | meeting_time = "#{meeting_time} EST".to_time 13 | reference_guid = md.xpath('//a').map do |a| 14 | a.attributes.map do |k,v| 15 | next unless k == 'href' 16 | next unless v.value.match(/Meeting.aspx.*/) 17 | next unless a.children.to_s.match(/HTML/) 18 | v.value.match(/Meeting.aspx\?Id=(?[^&]*)/)["id"] 19 | end 20 | end.flatten.compact.first 21 | 22 | next unless reference_guid 23 | 24 | attrs = { 25 | title: title, 26 | reference_guid: reference_guid, 27 | meeting_time: meeting_time 28 | } 29 | MeetingScanJob.perform_now(attrs: attrs) 30 | end 31 | 32 | puts "DevApp: (getting latest)" 33 | dev_apps = Set.new 34 | latest = DevApp::Scanner.latest 35 | latest.sort_by{|d| d[:status_date].to_date}.reverse.first(10).map{|d| d[:app_number]}.each do |a| 36 | if dev_apps.include?(a) 37 | puts "DevApp: #{a} (dup)" 38 | next 39 | end 40 | puts "DevApp: #{a}" 41 | dev_apps << a 42 | DevApp::Scanner.scan_application(a) 43 | end 44 | 45 | puts "ConsultationScanner" 46 | ConsultationScanner.perform_now 47 | 48 | puts "ZoningScanner" 49 | ZoningScanner.perform_now(allow_again: false) 50 | 51 | puts "ParcelScanner" 52 | ParcelScanner.perform_now(allow_again: false) 53 | 54 | meeting_type = "City Council" 55 | puts "MeetingScanJob: #{meeting_type}" 56 | meetings = MeetingScanJob.scan_past_meetings(meeting_type) 57 | meetings.sort{|d| d[:meeting_time]}.last(10).each do |m| 58 | puts "MeetingScanJob: #{m}" 59 | MeetingScanJob.new.send(:scan_meeting, meetings.sample) 60 | end 61 | 62 | (-10..0).each do |i| 63 | date = Date.today + i.days 64 | puts "LobbyingScanJob: #{date}" 65 | LobbyingScanJob.perform_now(date: date) 66 | end 67 | end 68 | 69 | desc "Speed data" 70 | task speed: :environment do 71 | url = "https://opendata.arcgis.com/api/v3/datasets/eb75a3d3c8d34e67a923e886ae006f90_0/downloads/data?format=csv&spatialRefId=4326" 72 | data = Net::HTTP.get(URI(url)) 73 | data = data.force_encoding("UTF-8").sub("\xEF\xBB\xBF", "") 74 | csv = CSV.new(data, headers: true) 75 | 76 | row = csv.first.to_h 77 | dates = row.keys.map{|k| k.match(/20\d\d_\d\d_\d\d/)&.to_s}.compact.uniq 78 | 79 | static_cols = [ 80 | "Location", 81 | "Camera_Install_Year", 82 | "Latitude", 83 | "Longitude", 84 | "X", 85 | "Y", 86 | "ObjectId", 87 | "ObjectId2" 88 | ] 89 | 90 | dynamic_cols = [ 91 | "AvgSpeed", 92 | "Pct85th", 93 | "PctCompliance", 94 | "PctHighEndSpeeders", 95 | ] 96 | 97 | headers = ["Date"] + static_cols + dynamic_cols 98 | 99 | puts CSV.generate(write_headers: true, headers: headers) do |out_csv| 100 | csv.each do |r| 101 | r = r.to_h 102 | dates.each do |d| 103 | o = r.slice(*static_cols) 104 | o["Date"] = d.gsub(/_/, '-') 105 | dynamic_cols.each do |k| 106 | o[k] = r["#{k}_#{d}"] 107 | end 108 | out_csv << o 109 | end 110 | end 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinodotnet/ottwatch/2e337b8583bb932234052e6c6a3a4aac516bcd5a/log/.keep -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinodotnet/ottwatch/2e337b8583bb932234052e6c6a3a4aac516bcd5a/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinodotnet/ottwatch/2e337b8583bb932234052e6c6a3a4aac516bcd5a/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinodotnet/ottwatch/2e337b8583bb932234052e6c6a3a4aac516bcd5a/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /storage/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinodotnet/ottwatch/2e337b8583bb932234052e6c6a3a4aac516bcd5a/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/kevinodotnet/ottwatch/2e337b8583bb932234052e6c6a3a4aac516bcd5a/test/controllers/.keep -------------------------------------------------------------------------------- /test/controllers/announcement_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class AnnouncementControllerTest < ActionDispatch::IntegrationTest 4 | test "should get index" do 5 | get announcement_index_url 6 | assert_response :success 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/controllers/devapp_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class DevappControllerTest < ActionDispatch::IntegrationTest 4 | test "#index works" do 5 | assert_equal 200, (get "/devapp/index") 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/home_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class HomeControllerTest < ActionDispatch::IntegrationTest 4 | test "should get index" do 5 | get home_index_url 6 | assert_response :success 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/controllers/lobbying_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class LobbyingControllerTest < ActionDispatch::IntegrationTest 4 | setup do 5 | 10.times do 6 | attr = { 7 | subject: "Don't allow driveways through Byron Linear Park, like EVER. Never, ever. Nope. #{rand(1000)}", 8 | issue: "the issue", 9 | lobbyist_name: "Lobbying McLobbyist", 10 | lobbyist_position: "CEO", 11 | lobbyist_reg_type: "ACTIVE", 12 | } 13 | undertaking = LobbyingUndertaking.create!(**attr) 14 | (rand(10)+5).times do 15 | attr = { 16 | activity_date: rand(300).days.ago, 17 | activity_type: "email", 18 | lobbied_name: "City McPerson", 19 | lobbied_title: "Staffer", 20 | } 21 | undertaking.activities.create!(attr) 22 | end 23 | end 24 | end 25 | 26 | test "#show for non-existant devapp fails cleanly" do 27 | get "/lobbying/index" 28 | assert_response :success 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/controllers/meeting_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class MeetingControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/parcels_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ParcelsControllerTest < ActionDispatch::IntegrationTest 4 | end 5 | -------------------------------------------------------------------------------- /test/controllers/team_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class TeamControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/traffic_cameras_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class TrafficCamerasControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/factories/campaign_donations.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | end 3 | -------------------------------------------------------------------------------- /test/factories/meeting_item_documents.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :meeting_item_document do 3 | reference_id { "MyString" } 4 | title { "MyString" } 5 | meeting_item_id { nil } 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/fixtures/committees.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | name: MyString 5 | 6 | two: 7 | name: MyString 8 | -------------------------------------------------------------------------------- /test/fixtures/dev_app/addresses.yml: -------------------------------------------------------------------------------- 1 | one: 2 | entry: :one 3 | ref_id: "__00XKWY" 4 | road_number: "101" 5 | qualifier: "" 6 | legal_unit: "" 7 | road_name: "WURTEMBURG" 8 | direction: "" 9 | road_type: "Street" 10 | municipality: "Old Ottawa" 11 | address_type: "MAIN" 12 | lat: 0.45435901e2 13 | lon: -0.75676664e2 14 | parcel_pin: "42360516" 15 | created_at: Thu, 10 Feb 2022 02:40:31.498201000 UTC +00:00 16 | updated_at: Thu, 10 Feb 2022 02:40:31.498201000 UTC +00:00> 17 | 18 | two: 19 | entry: :two 20 | ref_id: "__01N3FV" 21 | road_number: "3020" 22 | qualifier: "" 23 | legal_unit: "" 24 | road_name: "HAWTHORNE" 25 | direction: "" 26 | road_type: "Road" 27 | municipality: "Old Ottawa" 28 | address_type: "MAIN" 29 | lat: 0.45388191e2 30 | lon: -0.75608314e2 31 | parcel_pin: "41651043" 32 | -------------------------------------------------------------------------------- /test/fixtures/dev_app/documents.yml: -------------------------------------------------------------------------------- 1 | one: 2 | entry: one 3 | ref_id: "__0EZY7E" 4 | name: "October 2016 - D07-12-15-0205 Stormwater Management Plan" 5 | path: "Site Plan Application_Image Reference_October 2016 - D07-12-15-0205 Stormwater Management Plan.PDF" 6 | url: 7 | "http://webcast.ottawa.ca/plan/All_Image%20Referencing_Site%20Plan%20Application_Image%20Reference_October%202016%20-%20D07-12-15-0205%20Stormwater%20Management%20Plan.PDF" 8 | created_at: Thu, 10 Feb 2022 02:40:31.621695000 UTC +00:00 9 | updated_at: Thu, 10 Feb 2022 02:40:31.621695000 UTC +00:00> 10 | 11 | two: 12 | entry: two 13 | ref_id: "__0EZY10" 14 | name: "October 2016 - D07-12-15-0205 - Stormwater Management and Servicing Study" 15 | path: "Site Plan Application_Image Reference_October 2016 - D07-12-15-0205 - Stormwater Management and Servicing Study.PDF" 16 | url: 17 | "http://webcast.ottawa.ca/plan/All_Image%20Referencing_Site%20Plan%20Application_Image%20Reference_October%202016%20-%20D07-12-15-0205%20-%20Stormwater%20Management%20and%20Servicing%20Study.PDF" 18 | -------------------------------------------------------------------------------- /test/fixtures/dev_app/entries.yml: -------------------------------------------------------------------------------- 1 | one: 2 | app_id: "__01N3AR" 3 | app_number: "D07-12-15-0205" 4 | app_type: "Site Plan Control" 5 | 6 | two: 7 | app_id: "__00XKVT" 8 | app_number: "D07-05-16-0003" 9 | app_type: "Demolition Control" 10 | -------------------------------------------------------------------------------- /test/fixtures/dev_app/statuses.yml: -------------------------------------------------------------------------------- 1 | one: 2 | entry: :one 3 | status: Active 4 | 5 | two: 6 | entry: :two 7 | status: Inactive 8 | -------------------------------------------------------------------------------- /test/fixtures/files/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinodotnet/ottwatch/2e337b8583bb932234052e6c6a3a4aac516bcd5a/test/fixtures/files/.keep -------------------------------------------------------------------------------- /test/fixtures/files/dev_apps.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinodotnet/ottwatch/2e337b8583bb932234052e6c6a3a4aac516bcd5a/test/fixtures/files/dev_apps.xlsx -------------------------------------------------------------------------------- /test/fixtures/global_controls.yml: -------------------------------------------------------------------------------- 1 | one: 2 | name: k1 3 | value: v1 4 | 5 | two: 6 | name: k2 7 | value: v2 8 | -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinodotnet/ottwatch/2e337b8583bb932234052e6c6a3a4aac516bcd5a/test/helpers/.keep -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinodotnet/ottwatch/2e337b8583bb932234052e6c6a3a4aac516bcd5a/test/integration/.keep -------------------------------------------------------------------------------- /test/jobs/dev_app_scan_job_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class DevAppScanJobTest < ActiveJob::TestCase 4 | test "with no arguments, job runs in enqueuing mode" do 5 | DevApp::Scanner.expects(:latest).returns([{app_number: "D07-12-15-0115"}]) 6 | assert_enqueued_with(job: DevAppScanJob) do 7 | DevAppScanJob.perform_now 8 | end 9 | end 10 | 11 | test "does not enqueue duplicate jobs for same application id" do 12 | DevApp::Scanner.expects(:latest).returns( 13 | [ 14 | {app_number: "ONE"}, 15 | {app_number: "ONE"}, 16 | {app_number: "TWO"}, 17 | ] 18 | ) 19 | assert_enqueued_jobs(2) do 20 | DevAppScanJob.perform_now 21 | end 22 | end 23 | 24 | test "with specified app_number, job deep processes just that application" do 25 | DevApp::Scanner.expects(:latest).never 26 | DevApp::Scanner.expects(:scan_application).with("D07-12-15-0115") 27 | DevAppScanJob.perform_now(app_number: "D07-12-15-0115") 28 | end 29 | end -------------------------------------------------------------------------------- /test/jobs/lobbying_scan_job_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class LobbyingScanJobTest < ActiveJob::TestCase 4 | test "all details of a lobbying undertaking are captured" do 5 | VCR.use_cassette("#{class_name}_#{method_name}", :match_requests_on => [:body]) do 6 | assert_changes(->{LobbyingUndertaking.count}) do 7 | LobbyingScanJob.perform_now(date: "2025-02-21") 8 | end 9 | end 10 | assert LobbyingUndertaking.all.to_a.all?{|u| u.attributes.values.all?{|v| v.presence}} 11 | end 12 | 13 | test "specific dates can be scraped" do 14 | VCR.use_cassette("#{class_name}_#{method_name}", :match_requests_on => [:body]) do 15 | LobbyingScanJob.perform_now(date: "2022-04-12") 16 | end 17 | assert_equal 2, LobbyingUndertaking.count 18 | assert u = LobbyingUndertaking.find_by(subject: "Budget") 19 | assert_equal "Submission of OSLA's response to the federal budget 2022 to city council.", u.issue 20 | 21 | assert u = LobbyingUndertaking.find_by(subject: "By-law/Regulation") 22 | assert_equal "Dog bite prevention program for children", u.issue 23 | assert_records 24 | end 25 | 26 | test "lobbying records are scraped over many days" do 27 | LobbyingScanJob.expects(:perform_later).times(LobbyingScanJob::HISTORY_DAYS + 1) 28 | VCR.use_cassette("#{class_name}_#{method_name}", :match_requests_on => [:body]) do 29 | LobbyingScanJob.perform_now 30 | end 31 | end 32 | 33 | test "lobbying activity dates are parsed and saved correctly" do 34 | VCR.use_cassette("#{class_name}_#{method_name}", :match_requests_on => [:body]) do 35 | LobbyingScanJob.perform_now(date: "2022-03-23") 36 | end 37 | u = LobbyingUndertaking.last 38 | assert u.activities.count > 1 39 | end 40 | 41 | test "fix failure occurring for date:2023-01-30" do 42 | VCR.use_cassette("#{class_name}_#{method_name}", :match_requests_on => [:body]) do 43 | assert_nothing_raised do 44 | LobbyingScanJob.perform_now(date: "2023-01-30") 45 | end 46 | end 47 | end 48 | 49 | test "new lobbying activities are announced" do 50 | VCR.use_cassette("#{class_name}_#{method_name}", :match_requests_on => [:body]) do 51 | LobbyingScanJob.perform_now(date: "2022-03-23") # "new lobbying file" 52 | u = LobbyingUndertaking.where("issue like ?", "%Kaaj%").first 53 | u.activities.where('activity_date > ?', '2022-04-10').delete_all 54 | u.reload 55 | assert_equal 1, u.announcements.count 56 | count_1 = u.activities.count 57 | assert count_1 > 1 58 | LobbyingScanJob.perform_now(date: "2022-03-23") # "new activity on file" 59 | assert_equal 2, u.announcements.count 60 | count_2 = u.activities.count 61 | assert count_2 > count_1 62 | 63 | a = u.announcements.first 64 | assert_equal "New Lobbying undertaking", a.message 65 | assert_equal a.reference_context, "Reza Lotfalian (CTO): Kaaj Energy Inc. is in the planning stage to propose ..." 66 | assert_equal a.reference_link, "http://localhost:33000/lobbying/#{u.id}" 67 | 68 | a = u.announcements.last 69 | assert_equal "Additional Lobbying activity", a.message 70 | assert_equal a.reference_context, "Reza Lotfalian (CTO): Kaaj Energy Inc. is in the planning stage to propose ..." 71 | assert_equal a.reference_link, "http://localhost:33000/lobbying/#{u.id}" 72 | end 73 | end 74 | 75 | test "existing lobbying without an announcement dont get announced on re-scan" do 76 | VCR.use_cassette("#{class_name}_#{method_name}", :match_requests_on => [:body]) do 77 | LobbyingScanJob.perform_now(date: "2022-03-23") 78 | Announcement.delete_all 79 | assert_no_changes -> { Announcement.count } do 80 | LobbyingScanJob.perform_now(date: "2022-03-23") 81 | end 82 | end 83 | end 84 | 85 | private 86 | 87 | def assert_records 88 | [LobbyingUndertaking, LobbyingActivity].each do |klass| 89 | assert klass.count > 0 90 | klass.new.attributes.keys.each do |k| 91 | assert klass.where(k => nil).none? 92 | end 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /test/jobs/ping_job_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class PingJobTest < ActiveJob::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/jobs/syndication_job_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class SyndicationJobTest < ActiveJob::TestCase 4 | setup do 5 | Announcement.create!(message: "A first message", reference: DevApp::Entry.first) 6 | end 7 | 8 | test "syndication job updates last_id pointer" do 9 | SyndicationJob.any_instance.expects(:syndicate).times(1) 10 | assert_changes -> { GlobalControl.get("syndication_job_last_id") }, from: nil, to: Announcement.last.id.to_s do 11 | SyndicationJob.perform_now 12 | end 13 | end 14 | 15 | test "syndication job does not double announce" do 16 | SyndicationJob.any_instance.expects(:syndicate).times(1) 17 | SyndicationJob.perform_now 18 | SyndicationJob.any_instance.expects(:syndicate).times(0) 19 | SyndicationJob.perform_now 20 | end 21 | end -------------------------------------------------------------------------------- /test/jobs/traffic_camera_scrape_job_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TrafficCameraScrapeJobTest < ActiveJob::TestCase 4 | test "job calls TrafficCamera.scrape_all" do 5 | TrafficCamera.expects(:scrape_all).once 6 | TrafficCameraScrapeJob.perform_now 7 | end 8 | end 9 | 10 | -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinodotnet/ottwatch/2e337b8583bb932234052e6c6a3a4aac516bcd5a/test/mailers/.keep -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinodotnet/ottwatch/2e337b8583bb932234052e6c6a3a4aac516bcd5a/test/models/.keep -------------------------------------------------------------------------------- /test/models/announcement_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class AnnouncementTest < ActiveSupport::TestCase 4 | test "reference_context for LobbyingUndertaking format" do 5 | VCR.use_cassette("#{class_name}_#{method_name}", :match_requests_on => [:body]) do 6 | LobbyingScanJob.perform_now(date: "2022-03-23") 7 | assert Announcement.first 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/models/blue_sky_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | # https://bsky.app/profile/ottwatch-test.bsky.social 4 | 5 | class BlueSkyTest < ActiveSupport::TestCase 6 | test "#create_post posts successfully" do 7 | VCR.use_cassette("#{class_name}_#{method_name}", :match_requests_on => [:body]) do 8 | vcr_file_path = Rails.root.join("fixtures/vcr_cassettes/#{class_name}_#{method_name.gsub(/#/, '_')}.yml") 9 | time = (File.exist?(vcr_file_path) ? File.mtime(vcr_file_path) : Time.now).beginning_of_day 10 | travel_to(time) do 11 | post_text = "A test message for #{class_name}_#{method_name} at #{Time.now.strftime('%Y%m%d_%H%M%S')}, link https://ottwatch.ca" 12 | post = BlueSky.new.skeet(post_text) 13 | # ref = post["uri"].gsub(/.*\//, '') 14 | # puts "https://bsky.app/profile/ottwatch-test.bsky.social/post/#{ref}" 15 | assert_match /at:\/\/did:/, post["uri"] 16 | assert_equal "valid", post["validationStatus"] 17 | end 18 | end 19 | end 20 | end -------------------------------------------------------------------------------- /test/models/committee_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class CommitteeTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/consultation_scanner_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ConsultationScannerTest < ActiveSupport::TestCase 4 | test "big integration test dont judge me" do 5 | VCR.use_cassette("#{class_name}_#{method_name}") do 6 | assert_changes -> { Consultation.count } do 7 | ConsultationScanner.perform_now 8 | end 9 | 10 | # cherry pick attr tests 11 | c = Consultation.find_by_href("/beryl-gaffney-off-leash-dog-park") 12 | assert_equal "Beryl Gaffney Off-leash Dog Park", c.title 13 | assert_equal "archived", c.status 14 | assert_equal 1, c.announcements.count 15 | expected = "New Consultation: Beryl Gaffney Off-leash Dog Park" 16 | assert_equal expected, c.announcements.first.message 17 | 18 | # confirm only two known states 19 | assert_equal ["archived", "published"], Consultation.all.map{|c| c.status}.uniq.sort 20 | 21 | # confirm all hrefs are relative 22 | assert Consultation.all.pluck(:href).all?{|v| v.match(/^\//)} 23 | 24 | # re-scan does nothing 25 | assert_no_changes -> { Consultation.count } do 26 | ConsultationScanner.perform_now 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/models/consultation_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ConsultationTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/coordinates_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class CoordinatesTest < ActiveSupport::TestCase 4 | test "lat and lon are rounded to 5 decimal places for meter precision by default" do 5 | coordinates = Coordinates.new(45.420906, -75.689374) 6 | assert_equal 45.42091, coordinates.lat 7 | assert_equal -75.68937, coordinates.lon 8 | end 9 | 10 | test "lat and lon are rounded to the specified precision" do 11 | coordinates = Coordinates.new(45.420906, -75.689374, precision: 2) 12 | assert_equal 45.42, coordinates.lat 13 | assert_equal -75.69, coordinates.lon 14 | end 15 | 16 | test "full_lat and full_lon are the original values" do 17 | coordinates = Coordinates.new(45.420906, -75.689374) 18 | assert_equal 45.420906, coordinates.full_lat 19 | assert_equal -75.689374, coordinates.full_lon 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/models/dev_app/address_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class DevApp::AddressTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/dev_app/document_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class DevApp::DocumentTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/dev_app/entry_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class DevApp::EntryTest < ActiveSupport::TestCase 4 | test "#current_status returns sane default if no statuses exist in DB" do 5 | entry = DevApp::Entry.first 6 | entry.statuses.destroy_all 7 | assert_changes -> { DevApp::Status.count } do 8 | assert_equal "404_missing_data", entry.current_status.status 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/models/dev_app/status_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class DevApp::StatusTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/global_control_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class GlobalControlTest < ActiveSupport::TestCase 4 | test "can set and get" do 5 | name = "the_key" 6 | value = "the_value" 7 | assert_difference -> { GlobalControl.count } do 8 | assert_nil GlobalControl.get(name) 9 | GlobalControl.set(name, value) 10 | assert_equal value, GlobalControl.get(name) 11 | GlobalControl.set(name, "val2") 12 | assert_equal "val2", GlobalControl.get(name) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/models/lobbying_activity_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class LobbyingActivityTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/lobbying_undertaking_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class LobbyingUndertakingTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/meeting_item_document_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class MeetingItemDocumentTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/meeting_item_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class MeetingItemTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/meeting_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class MeetingTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/parcel_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ParcelTest < ActiveSupport::TestCase 4 | test "#perform loads new entries starting with largest objectid" do 5 | VCR.use_cassette("#{class_name}_#{method_name}") do 6 | assert_difference -> { Parcel.count }, 1000 do 7 | ParcelScanner.new.perform 8 | end 9 | assert_difference -> { Parcel.count }, 1000 do 10 | ParcelScanner.new.perform 11 | end 12 | end 13 | end 14 | 15 | test "#perform uses first day of month as snapshot_date and pulls a full clone each month" do 16 | VCR.use_cassette("#{class_name}_#{method_name}") do 17 | travel_to(Time.zone.local(2023, 12, 15, 01, 02, 03)) do 18 | ParcelScanner.new.perform 19 | end 20 | travel_to(Time.zone.local(2024, 01, 15, 01, 02, 03)) do 21 | ParcelScanner.new.perform 22 | end 23 | assert_equal 2, Parcel.where(objectid: 1).count 24 | assert_equal ["2023-12-01".to_date, "2024-01-01".to_date], Parcel.where(objectid: 1).map(&:snapshot_date).sort 25 | end 26 | end 27 | 28 | test "#objects_after returns objects after the given one" do 29 | VCR.use_cassette("#{class_name}_#{method_name}") do 30 | features = ParcelScanner.new.objects_after(0) 31 | assert_equal 1000, features.count 32 | 33 | feature = features.first 34 | assert_equal 1, feature.dig("attributes", "OBJECTID") 35 | 36 | feature = features.last 37 | assert_equal 1000, feature.dig("attributes", "OBJECTID") 38 | 39 | # ... and the format is as expected 40 | expected = [ 41 | "OBJECTID", 42 | "PIN", 43 | "EASTING", 44 | "NORTHING", 45 | "PUBLICLAND", 46 | "PARCELTYPE", 47 | "TEXTHEIGHT", 48 | "TEXTWIDTH", 49 | "TEXTROTATION", 50 | "PI_MUNICIPAL_ADDRESS_ID", 51 | "RECORD_OWNER_ID", 52 | "RT_ROAD_NAME_ID", 53 | "ADDRESS_NUMBER", 54 | "ROAD_NAME", 55 | "SUFFIX", 56 | "DIR", 57 | "MUNICIPALITY_NAME", 58 | "LEGAL_UNIT", 59 | "ADDRESS_QUALIFIER", 60 | "POSTAL_CODE", 61 | "ADDRESS_STATUS", 62 | "ADDRESS_TYPE_ID", 63 | "PIN_NUMBER", 64 | "FEAT_NUM", 65 | "PI_PARCEL_ID", 66 | "Shape_Length", 67 | "Shape_Area" 68 | ] 69 | assert_equal expected, feature.dig("attributes").keys 70 | 71 | # ... and we can save it as Parcel 72 | assert_difference -> { Parcel.count } do 73 | ParcelScanner.new.send(:parcel_from_api, feature) 74 | end 75 | assert_no_difference -> { Parcel.count } do 76 | ParcelScanner.new.send(:parcel_from_api, feature) 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /test/models/traffic_camera_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TrafficCameraTest < ActiveSupport::TestCase 4 | setup do 5 | @camera = TrafficCamera.create!(reference_id: 37, camera_number: 8, name: "Belfast & St. Laurent", camera_owner: "CITY", lat: 45.411858, lon: -75.630376) 6 | system("sqlite3 #{TrafficCamera::CAPTURE_FOLDER}/camera_archive.sqlar -Ac") 7 | end 8 | 9 | 10 | test "cameras are scraped correctly" do 11 | VCR.use_cassette("#{class_name}_#{method_name}", :match_requests_on => [:body]) do 12 | cameras =TrafficCamera.cameras 13 | TrafficCamera.scrape_all 14 | assert cameras.count > 300 15 | assert_equal cameras.count, TrafficCamera.count 16 | end 17 | end 18 | 19 | test "cameras are not duplicated when scraped again" do 20 | VCR.use_cassette("#{class_name}_#{method_name}", :match_requests_on => [:body]) do 21 | TrafficCamera.scrape_all 22 | assert_no_difference -> { TrafficCamera.count } do 23 | TrafficCamera.scrape_all 24 | end 25 | end 26 | end 27 | 28 | test "traffic camera images are captured correctly" do 29 | VCR.use_cassette("#{class_name}_#{method_name}", :match_requests_on => [:body]) do 30 | assert_difference -> { captured_files.count } do 31 | @camera.capture_image 32 | end 33 | end 34 | end 35 | 36 | test "#captures returns the correct captures" do 37 | VCR.use_cassette("#{class_name}_#{method_name}", :match_requests_on => [:body]) do 38 | assert_difference -> { @camera.captures.count } do 39 | @camera.capture_image 40 | end 41 | end 42 | capture = @camera.captures.last 43 | end 44 | 45 | test "#captures inserts a row in the SQLite archive" do 46 | VCR.use_cassette("#{class_name}_#{method_name}", :match_requests_on => [:body]) do 47 | image_data = @camera.capture_image 48 | SQLite3::Database.open(TrafficCamera::SQLITE_ARCHIVE) do |db| 49 | assert_equal db.execute(" select count(1) from sqlar ").first.first, 1 50 | end 51 | db_data = @camera.image_from_sqlite_archive(image_data[:time]) 52 | assert_equal image_data[:image], db_data 53 | end 54 | end 55 | 56 | private 57 | 58 | def captured_files 59 | Dir.glob(File.join(TrafficCamera::CAPTURE_FOLDER, '**', '*')).select { |f| File.file?(f) }.sort 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /test/models/user_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class UserTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/zoning_scanner_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ZoningScannerTest < ActiveSupport::TestCase 4 | test "#perform starts at 0 and moves forward in steps of 1000" do 5 | VCR.use_cassette("#{class_name}_#{method_name}") do 6 | assert_difference -> { Zoning.count }, 1000 do 7 | ZoningScanner.new.perform 8 | end 9 | assert_difference -> { Zoning.count }, 1000 do 10 | ZoningScanner.new.perform 11 | end 12 | end 13 | end 14 | 15 | test "#perform uses first day of month as snapshot_date and pulls a full clone each month" do 16 | VCR.use_cassette("#{class_name}_#{method_name}") do 17 | travel_to(Time.zone.local(2023, 12, 15, 01, 02, 03)) do 18 | ZoningScanner.new.perform 19 | end 20 | assert_equal "2023-12-01".to_date, Zoning.first.snapshot_date 21 | end 22 | end 23 | end -------------------------------------------------------------------------------- /test/models/zoning_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ZoningTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/system/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinodotnet/ottwatch/2e337b8583bb932234052e6c6a3a4aac516bcd5a/test/system/.keep -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] ||= "test" 2 | require_relative "../config/environment" 3 | require "rails/test_help" 4 | 5 | require "minitest/unit" 6 | require "mocha/minitest" 7 | require "vcr" 8 | require "webmock" 9 | 10 | class ActiveSupport::TestCase 11 | # Run tests in parallel with specified workers 12 | parallelize(workers: :number_of_processors) 13 | 14 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 15 | fixtures :all 16 | 17 | # Add more helper methods to be used by all tests here... 18 | 19 | VCR.configure do |config| 20 | config.cassette_library_dir = "fixtures/vcr_cassettes" 21 | config.hook_into :webmock 22 | end 23 | end 24 | 25 | class Minitest::Unit::TestCase 26 | include FactoryBot::Syntax::Methods 27 | end 28 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinodotnet/ottwatch/2e337b8583bb932234052e6c6a3a4aac516bcd5a/tmp/.keep -------------------------------------------------------------------------------- /tmp/pids/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinodotnet/ottwatch/2e337b8583bb932234052e6c6a3a4aac516bcd5a/tmp/pids/.keep -------------------------------------------------------------------------------- /tmp/storage/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinodotnet/ottwatch/2e337b8583bb932234052e6c6a3a4aac516bcd5a/tmp/storage/.keep -------------------------------------------------------------------------------- /vendor/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinodotnet/ottwatch/2e337b8583bb932234052e6c6a3a4aac516bcd5a/vendor/.keep -------------------------------------------------------------------------------- /vendor/javascript/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinodotnet/ottwatch/2e337b8583bb932234052e6c6a3a4aac516bcd5a/vendor/javascript/.keep --------------------------------------------------------------------------------