├── .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 | #
<%= 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 |
62 | <%= @entry.planner_first_name %> <%= @entry.planner_last_name %>
63 | <%= @entry.planner_email %>
64 | <%= @entry.planner_phone %>
65 |
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 |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 |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 |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 |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 |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 |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 |86 | 87 | Catch the old-school RSS feed: you know what to do. 88 |
89 |
9 | <%= @undertaking.issue %>
10 | (<%= @undertaking.subject %>)
11 |
<%= k %> | 11 |<%= v %> | 12 |
---|
2 | Congrats @<%= current_user.username %>
3 | You're a member of the team!
4 |
7 | How many users are there?
8 | There are <%= User.count %> users.
9 |
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 |
17 | What's next?
18 |
<%= Time.now.in_time_zone("America/New_York").strftime('%Y-%m-%d %H:%M:%S') %>
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=(?You may have mistyped the address or the page may have moved.
63 |If you are the application owner check the logs for more information.
65 |Maybe you tried to change something you didn't have access to.
63 |If you are the application owner check the logs for more information.
65 |If you are the application owner check the logs for more information.
64 |