├── .circleci └── config.yml ├── .env.example ├── .flooignore ├── .gitignore ├── .overcommit.yml ├── .rspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── .ruby-version ├── DEVELOPMENT.md ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Procfile ├── README.md ├── Rakefile ├── app ├── admin │ ├── client.rb │ ├── client_status.rb │ ├── court_date_csv.rb │ ├── department.rb │ ├── feature_flag.rb │ ├── highlight_blob.rb │ ├── import_csv.rb │ ├── reporting_relationship.rb │ ├── reporting_relationship_client.rb │ ├── reports.rb │ └── user.rb ├── assets │ ├── config │ │ ├── court_locations.csv │ │ └── manifest.js │ ├── fonts │ │ ├── icomoon.eot │ │ ├── icomoon.svg │ │ ├── icomoon.ttf │ │ └── icomoon.woff │ ├── images │ │ ├── .keep │ │ ├── emojis │ │ │ ├── 1f35a.png │ │ │ ├── 1f373.png │ │ │ ├── 1f392.png │ │ │ ├── 1f3e0.png │ │ │ ├── 1f447.png │ │ │ ├── 1f44c.png │ │ │ ├── 1f464.png │ │ │ ├── 1f4a1.png │ │ │ ├── 1f4b0.png │ │ │ ├── 1f4b2.png │ │ │ ├── 1f4b3.png │ │ │ ├── 1f4b5.png │ │ │ ├── 1f4bb.png │ │ │ ├── 1f4c4.png │ │ │ ├── 1f4cb.png │ │ │ ├── 1f4dd.png │ │ │ ├── 1f4eb.png │ │ │ ├── 1f4f2.png │ │ │ ├── 1f512.png │ │ │ ├── 1f600.png │ │ │ ├── 1f60a.png │ │ │ ├── 1f610.png │ │ │ ├── 1f641.png │ │ │ ├── 1f6e1.png │ │ │ ├── 1f914.png │ │ │ ├── 1f951.png │ │ │ ├── 1f955.png │ │ │ ├── 1f957.png │ │ │ ├── 1f958.png │ │ │ ├── 260e.png │ │ │ ├── 2705.png │ │ │ ├── 270a.png │ │ │ ├── 270f.png │ │ │ └── 2712.png │ │ ├── empty_templates.png │ │ ├── favicon.ico │ │ ├── favicon.read.png │ │ ├── favicon.unread.png │ │ ├── handshake.png │ │ ├── handshake.svg │ │ ├── logo.png │ │ ├── mailbox.svg │ │ └── open-mailbox-with-raised-flag.svg │ ├── javascripts │ │ ├── active_admin.js │ │ ├── application.js │ │ ├── cable.js │ │ ├── channels │ │ │ ├── .keep │ │ │ ├── clients.js │ │ │ ├── events.js │ │ │ ├── messages.js │ │ │ ├── notifications.js │ │ │ └── scheduled_messages.js │ │ ├── client_search.js │ │ ├── clientcomm.js │ │ ├── clients.js │ │ ├── clients │ │ │ ├── edit.js │ │ │ └── new.js │ │ ├── errors.js │ │ ├── lib │ │ │ └── list.min.js │ │ ├── mass_messages.js │ │ ├── messages.js │ │ ├── reporting_relationships │ │ │ └── welcomes.js │ │ ├── scheduled_messages.js │ │ └── twemoji-init.js │ └── stylesheets │ │ ├── _ie-hacks.scss │ │ ├── _shame.scss │ │ ├── active_admin.scss │ │ ├── application.scss │ │ ├── atoms │ │ ├── _base.scss │ │ ├── _buttons.scss │ │ ├── _clientcomm_grid.scss │ │ ├── _emoji.scss │ │ ├── _form-elements.scss │ │ ├── _form-widths.scss │ │ ├── _grid.scss │ │ ├── _icon_mods.scss │ │ ├── _icons.scss │ │ ├── _illustrations.scss │ │ ├── _images.scss │ │ ├── _labels.scss │ │ ├── _layout.scss │ │ ├── _typography.scss │ │ ├── _utilities.scss │ │ └── _variables.scss │ │ ├── clientcomm.scss │ │ ├── email.scss │ │ ├── mailers │ │ └── mailer.scss │ │ ├── molecules │ │ ├── _data-table.scss │ │ ├── _flashes.scss │ │ ├── _form-molecules.scss │ │ ├── _incrementer.scss │ │ ├── _media-box.scss │ │ ├── _progress-indicator.scss │ │ ├── _reveal.scss │ │ ├── _scroller.scss │ │ ├── _searchbar.scss │ │ ├── _sendbar.scss │ │ ├── _steps.scss │ │ ├── _summary-table.scss │ │ ├── _tabs.scss │ │ ├── _template_popover.scss │ │ ├── _toolbar-responsive.scss │ │ └── _toolbar.scss │ │ ├── organisms │ │ ├── _demo-banner.scss │ │ ├── _document-preview.scss │ │ ├── _form-card.scss │ │ ├── _legal.scss │ │ ├── _main-footer.scss │ │ ├── _main-header.scss │ │ ├── _pagination.scss │ │ └── _statistic-card.scss │ │ └── templates │ │ ├── _about.scss │ │ ├── _benefitscal.scss │ │ ├── _clients.scss │ │ ├── _dashboard.scss │ │ ├── _error.scss │ │ ├── _for-counties.scss │ │ ├── _homepage.scss │ │ ├── _mass-messages.scss │ │ ├── _messages.scss │ │ ├── _numbers.scss │ │ ├── _question.scss │ │ ├── _static-page.scss │ │ ├── _styleguide.scss │ │ └── _welcome-form.scss ├── channels │ ├── application_cable │ │ ├── channel.rb │ │ └── connection.rb │ ├── clients_channel.rb │ ├── events_channel.rb │ ├── messages_channel.rb │ ├── notifications_channel.rb │ └── scheduled_messages_channel.rb ├── controllers │ ├── application_controller.rb │ ├── clients_controller.rb │ ├── concerns │ │ └── .keep │ ├── errors_controller.rb │ ├── file_previews_controller.rb │ ├── help_controller.rb │ ├── mass_messages_controller.rb │ ├── merge_reporting_relationships_controller.rb │ ├── messages │ │ └── reads_controller.rb │ ├── messages_controller.rb │ ├── reporting_relationships │ │ └── welcomes_controller.rb │ ├── reporting_relationships_controller.rb │ ├── scheduled_messages_controller.rb │ ├── tracking_events_controller.rb │ ├── twilio_controller.rb │ └── users │ │ ├── confirmations_controller.rb │ │ ├── omniauth_callbacks_controller.rb │ │ ├── passwords_controller.rb │ │ ├── registrations_controller.rb │ │ ├── sessions_controller.rb │ │ └── unlocks_controller.rb ├── helpers │ ├── analytics_helper.rb │ ├── application_helper.rb │ └── gcf_form_builder.rb ├── jobs │ ├── application_job.rb │ ├── create_court_reminders_job.rb │ ├── dead_man_switch_job.rb │ ├── incoming_message_job.rb │ ├── message_broadcast_job.rb │ ├── message_redaction_job.rb │ ├── notification_broadcast_job.rb │ ├── scheduled_message_cron_job.rb │ ├── scheduled_message_job.rb │ └── send_mass_message_job.rb ├── lib │ ├── active_admin_adapter.rb │ ├── analytics_service.rb │ ├── court_reminders_importer.rb │ ├── date_parser.rb │ ├── fuzzy_matcher.rb │ ├── list_maker.rb │ ├── message_alert_builder.rb │ ├── message_handler.rb │ ├── notification_sender.rb │ ├── phone_number_parser.rb │ ├── retrier.rb │ ├── sms_service.rb │ └── voice_service.rb ├── mailers │ ├── application_mailer.rb │ └── notification_mailer.rb ├── models │ ├── application_record.rb │ ├── attachment.rb │ ├── change_image.rb │ ├── client.rb │ ├── client_edit_marker.rb │ ├── client_status.rb │ ├── concerns │ │ └── .keep │ ├── conversation_ends_marker.rb │ ├── court_date_csv.rb │ ├── court_reminder.rb │ ├── department.rb │ ├── feature_flag.rb │ ├── highlight_blob.rb │ ├── marker.rb │ ├── mass_message.rb │ ├── merge_client.rb │ ├── merge_reporting_relationship.rb │ ├── merged_with_marker.rb │ ├── message.rb │ ├── report.rb │ ├── reporting_relationship.rb │ ├── survey.rb │ ├── survey_question.rb │ ├── survey_response.rb │ ├── survey_response_link.rb │ ├── text_message.rb │ ├── transfer_marker.rb │ └── user.rb └── views │ ├── admin │ └── users │ │ └── disable.html.erb │ ├── attachments │ ├── _attachment.html.erb │ └── _inline_attachment.html.erb │ ├── client_edit_markers │ └── _client_edit_marker.html.erb │ ├── clients │ ├── _client_form.html.erb │ ├── _client_manage.html.erb │ ├── _client_status.html.erb │ ├── _clients.html.erb │ ├── confirm.html.erb │ ├── edit.html.erb │ ├── index.html.erb │ ├── index.js.erb │ └── new.html.erb │ ├── conversation_ends_markers │ └── _conversation_ends_marker.html.erb │ ├── court_reminders │ └── _court_reminder.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 │ ├── shared │ │ └── _links.html.erb │ └── unlocks │ │ └── new.html.erb │ ├── errors │ ├── internal_server_error.html.erb │ └── not_found.html.erb │ ├── layouts │ ├── _flash.html.erb │ ├── application.html.erb │ ├── mailer.html.erb │ └── mailer.text.erb │ ├── mass_messages │ └── new.html.erb │ ├── merged_with_markers │ └── _merged_with_marker.html.erb │ ├── messages │ └── transcript_download.txt.erb │ ├── notification_mailer │ ├── batch_transfer_notification.html.erb │ ├── client_edit_notification.html.erb │ ├── client_transfer_notification.html.erb │ ├── court_reminders_failure.html.erb │ ├── court_reminders_success.html.erb │ ├── message_notification.html.erb │ ├── message_notification.text.erb │ └── report_usage.html.erb │ ├── reporting_relationships │ ├── _edit_message_modal.html.erb │ ├── _scheduled_messages_link.html.erb │ ├── _send_message_form.html.erb │ ├── show.html.erb │ └── welcomes │ │ └── new.html.erb │ ├── scheduled_messages │ ├── _scheduled_list_modal.html.erb │ └── index.html.erb │ ├── text_messages │ ├── _text_message.html.erb │ └── _text_message_status.html.erb │ ├── transfer_markers │ └── _transfer_marker.html.erb │ └── users │ ├── registrations │ └── edit.html.erb │ └── sessions │ └── new.html.erb ├── bin ├── brakeman ├── bundle ├── bundler-audit ├── codeclimate ├── delayed_job ├── pushit ├── rails ├── rake ├── rspec ├── setup ├── spring ├── test ├── update └── yarn ├── co-authors ├── config.ru ├── config ├── application.rb ├── boot.rb ├── brakeman.ignore ├── cable.yml ├── config.yml ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ ├── staging.rb │ └── test.rb ├── initializers │ ├── active_admin.rb │ ├── application_controller_renderer.rb │ ├── assets.rb │ ├── backtrace_silencers.rb │ ├── cloudwatch.rb │ ├── config.rb │ ├── cookies_serializer.rb │ ├── delayed_job.rb │ ├── devise.rb │ ├── fake_twilio.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ ├── intercom.rb │ ├── mime_types.rb │ ├── mixpanel.rb │ ├── new_framework_defaults.rb │ ├── new_framework_defaults_5_1.rb │ ├── paperclip.rb │ ├── premailer_rails.rb │ ├── session_store.rb │ ├── time_formats.rb │ └── wrap_parameters.rb ├── locales │ ├── devise.en.yml │ └── en.yml ├── puma.rb ├── routes.rb ├── secrets.yml └── spring.rb ├── db ├── migrate │ ├── 20170511080820_create_clients.rb │ ├── 20170512212117_devise_create_users.rb │ ├── 20170517213000_add_user_ref_to_clients.rb │ ├── 20170519034001_add_full_name_to_users.rb │ ├── 20170519215508_create_messages.rb │ ├── 20170523081015_add_index_to_messages.rb │ ├── 20170606151721_create_delayed_jobs.rb │ ├── 20170608050028_add_read_to_messages.rb │ ├── 20170619175734_create_attachments.rb │ ├── 20170706230335_add_send_date_to_messages.rb │ ├── 20170711222129_devise_invitable_add_to_users.rb │ ├── 20170714175419_rename_send_date_to_send_at_in_messages.rb │ ├── 20170718001857_remove_birth_date_from_clients.rb │ ├── 20170728173551_add_email_subscribe_to_user.rb │ ├── 20170731233514_add_archived_to_client.rb │ ├── 20170801221946_add_lock_version_to_messages.rb │ ├── 20170801222108_add_sent_to_messages.rb │ ├── 20170803184914_set_send_at_on_messages.rb │ ├── 20170804173345_remove_archived_from_client.rb │ ├── 20170815171031_devise_create_admin_users.rb │ ├── 20170815171039_create_active_admin_comments.rb │ ├── 20170823233745_add_notes_to_clients.rb │ ├── 20170901230857_add_last_contacted_at_to_clients.rb │ ├── 20170911173225_add_index_to_clients.rb │ ├── 20170911215234_add_active_to_user.rb │ ├── 20170913221609_add_has_message_error_to_clients.rb │ ├── 20170920220501_make_last_contacted_at_nullable.rb │ ├── 20170926210630_create_templates.rb │ ├── 20170926211409_add_templates_ref_to_users.rb │ ├── 20170929175554_add_feature_flags_table.rb │ ├── 20171003212709_rename_attachment_to_legacy_attachment.rb │ ├── 20171003215315_create_paperclip_attachments.rb │ ├── 20171004220005_migrate_old_attachments.rb │ ├── 20171009214412_remove_devise_invitable.rb │ ├── 20171009234615_remove_signups.rb │ ├── 20171009235722_make_full_name_non_nullable.rb │ ├── 20171010185346_add_desk_phone_number_to_user.rb │ ├── 20171016161803_rename_user_desk_phone_number_to_phone_number.rb │ ├── 20171016173705_add_unclaimed_user_to_unclaimed_clients.rb │ ├── 20171017184404_add_client_statuses_table.rb │ ├── 20171020012824_add_follup_up_date_to_client_statuses.rb │ ├── 20171021000847_rename_email_subscribe_to_message_notification_emails.rb │ ├── 20171030170455_create_departments.rb │ ├── 20171030170543_add_departments_to_users.rb │ ├── 20171031180320_create_reporting_relationships.rb │ ├── 20171101221037_add_active_to_reporting_relationship.rb │ ├── 20171107212003_move_active_from_clients_to_reporting_relationships.rb │ ├── 20171109222018_add_user_to_department.rb │ ├── 20171129184319_add_metadata_values_to_reporting_relationship.rb │ ├── 20180105005217_create_survey_questions.rb │ ├── 20180105005654_create_survey_responses.rb │ ├── 20180105010123_create_surveys.rb │ ├── 20180105182759_create_survey_response_links.rb │ ├── 20180112232629_remove_columns_from_client.rb │ ├── 20180201222656_add_last_twilio_update_to_message.rb │ ├── 20180206221711_add_transfer_marker_to_messages.rb │ ├── 20180209191924_create_reports.rb │ ├── 20180220183545_add_icon_color_to_client_status.rb │ ├── 20180220194359_add_department_to_client_status.rb │ ├── 20180222191401_add_in_treatment_group_to_users.rb │ ├── 20180222202720_remove_in_treatment_group_from_users.rb │ ├── 20180228011744_make_followup_date_nullable_on_client_statuses.rb │ ├── 20180305231922_add_autoreply_to_department.rb │ ├── 20180307215710_add_treatment_group_to_user.rb │ ├── 20180311020035_change_user_treatment_group_to_string.rb │ ├── 20180314000756_add_reporting_relationship_to_message.rb │ ├── 20180326220423_move_message_transfer_marker_to_marker_type.rb │ ├── 20180411193449_add_node_id_to_user.rb │ ├── 20180413174714_add_node_ids_to_client.rb │ ├── 20180419203735_add_category_to_reporting_relationship.rb │ ├── 20180426170351_add_category_feature_flag.rb │ ├── 20180507185215_add_like_message_to_message.rb │ ├── 20180515185335_add_type_to_messages.rb │ ├── 20180516173227_add_id_number_to_clients.rb │ ├── 20180516175755_create_client_id_feature_flag.rb │ ├── 20180516183029_create_hide_notes_feature_flag.rb │ ├── 20180518203229_create_court_date_csv.rb │ ├── 20180605211155_add_admin_user_to_court_date_csv.rb │ ├── 20180606182912_add_court_date_csv_to_message.rb │ ├── 20180608172907_make_to_and_from_nullable_on_message.rb │ ├── 20180613212947_add_court_date_to_client.rb │ ├── 20180618172611_add_court_dates_feature_flag.rb │ ├── 20180618234037_add_scheduled_message_count_feature_flag.rb │ ├── 20180620203710_add_next_court_date_set_by_user_to_client.rb │ ├── 20180626210517_add_user_client_index_to_reporting_relationship.rb │ ├── 20180627173300_remove_locking_from_messages.rb │ ├── 20180627210514_mark_inactive_messages_as_read.rb │ ├── 20180628172800_add_dimensions_to_attachments.rb │ ├── 20180703172919_create_change_image.rb │ ├── 20180703180216_add_admin_user_to_change_image.rb │ ├── 20180719184142_add_has_unread_messages_to_user.rb │ ├── 20180724180654_add_cron_to_delayed_jobs.rb │ ├── 20180730232332_add_index_to_messages_send_at.rb │ ├── 20180731175831_drop_active_admin_comments_table.rb │ ├── 20180731212736_add_admin_flag_to_user.rb │ ├── 20180731213237_move_admin_users_to_users.rb │ ├── 20180801175551_drop_admin_users.rb │ ├── 20180806183847_create_highlight_blobs.rb │ ├── 20180813181128_drop_templates.rb │ ├── 20180816181102_remove_scheduled_message_jobs.rb │ ├── 20180816222016_cleanup_message_history.rb │ └── 20180921232040_add_active_to_survey_responses.rb ├── schema.rb └── seeds.rb ├── deploy ├── playbooks │ ├── data.yml │ ├── inventories │ │ └── cc-gwen │ │ │ ├── group_vars │ │ │ └── all │ │ │ │ └── vars │ │ │ └── hosts │ └── roles │ │ └── users │ │ ├── defaults │ │ └── main.yml │ │ ├── pubkeys │ │ ├── andrew.pub │ │ ├── andrewhyder.pub │ │ ├── anule.pub │ │ ├── ashcampo.pub │ │ ├── bensheldon.pub │ │ ├── bgolder.pub │ │ ├── eric.pub │ │ ├── gwen.pub │ │ ├── mikela.pub │ │ ├── tgrathwell.pub │ │ ├── tomas.pub │ │ └── zak.pub │ │ └── tasks │ │ └── main.yml └── terraform │ ├── README.md │ ├── app │ ├── app.tf │ ├── apply.sh │ └── plan.sh │ ├── apply.sh │ ├── clientcomm.tf │ ├── deploy │ ├── email │ ├── apply.sh │ ├── email.tf │ └── plan.sh │ └── plan.sh ├── heroku_run_on_all_deploys ├── lib ├── assets │ └── .keep ├── scripts │ ├── copy_database │ └── heroku_run_copy_database └── tasks │ ├── .keep │ ├── delayed_cron_job.rake │ ├── messages.rake │ ├── notes_to_id_number.rake │ ├── reports.rake │ ├── setup.rake │ ├── surveys.rake │ └── utils.rake ├── package-lock.json ├── public ├── 422.html ├── apple-touch-icon-precomposed.png ├── apple-touch-icon.png ├── clientcomm_01.png ├── clientcomm_02.png ├── clientcomm_03.png ├── favicon.ico ├── maintenance.html ├── robots.txt ├── twilio_alert.png ├── twilio_deploy.png └── twilio_develop.png ├── release-tasks.sh ├── spec ├── channels │ ├── clients_channel_spec.rb │ ├── messages_channel_spec.rb │ └── scheduled_messages_channel_spec.rb ├── factories │ ├── attachments.rb │ ├── change_image.rb │ ├── client_edit_marker.rb │ ├── client_statuses.rb │ ├── clients.rb │ ├── conversation_ends_marker.rb │ ├── court_date_csvs.rb │ ├── court_reminders.rb │ ├── departments.rb │ ├── highlight_blobs.rb │ ├── merged_with_marker.rb │ ├── reporting_relationships.rb │ ├── reports.rb │ ├── survey_questions.rb │ ├── survey_response_links.rb │ ├── survey_responses.rb │ ├── surveys.rb │ ├── templates.rb │ ├── text_message.rb │ ├── transfer_marker.rb │ └── users.rb ├── fake_twilio_client.rb ├── features │ ├── account_settings_spec.rb │ ├── admin_departments_spec.rb │ ├── admin_spec.rb │ ├── client_last_contact_is_date_of_last_message_spec.rb │ ├── client_list_search_and_sort_spec.rb │ ├── client_status_spec.rb │ ├── feature_flags_spec.rb │ ├── twilio_posts_new_message_spec.rb │ ├── twilio_posts_new_message_with_no_user_spec.rb │ ├── twilio_posts_status_update_spec.rb │ ├── user_creates_client_spec.rb │ ├── user_deactivates_client_spec.rb │ ├── user_edits_client_spec.rb │ ├── user_edits_court_reminder_spec.rb │ ├── user_edits_scheduled_message_spec.rb │ ├── user_gets_notification_with_message_count_spec.rb │ ├── user_has_conversation_with_client_spec.rb │ ├── user_has_incoming_notifications_spec.rb │ ├── user_login_spec.rb │ ├── user_merges_clients_spec.rb │ ├── user_receives_new_message_marked_read_spec.rb │ ├── user_recieves_messages_but_is_not_focused.rb │ ├── user_sends_images_to_client_spec.rb │ ├── user_sends_mass_message_spec.rb │ ├── user_sends_message_that_errors_spec.rb │ ├── user_transfers_client_spec.rb │ ├── user_views_court_reminder_on_conversation_page_spec.rb │ ├── user_visits_clients_page_spec.rb │ └── user_visits_messages_page_spec.rb ├── fixtures │ ├── bad_court_dates.csv │ ├── cat_contact.vcf │ ├── court_dates.csv │ ├── court_locs.csv │ ├── fluffy_cat.jpg │ ├── large_image.jpg │ ├── test_import_one_client.csv │ └── test_import_two_clients.csv ├── helpers │ ├── analytics_helper_spec.rb │ └── gcf_form_builder_spec.rb ├── javascripts │ ├── helpers │ │ ├── .gitkeep │ │ └── jasmine-jquery.js │ ├── messages_spec.js │ ├── replace_emoji_spec.js │ └── support │ │ └── jasmine.yml ├── jobs │ ├── create_court_reminders_job_spec.rb │ ├── incoming_message_job_spec.rb │ ├── mass_message_job_spec.rb │ ├── message_broadcast_job_spec.rb │ ├── message_redaction_job_spec.rb │ ├── scheduled_message_cron_job_spec.rb │ └── scheduled_message_job_spec.rb ├── lib │ ├── analytics_service_spec.rb │ ├── court_reminders_importer_spec.rb │ ├── date_parser_spec.rb │ ├── message_alert_builder_spec.rb │ ├── phone_number_parser_spec.rb │ ├── retrier_spec.rb │ ├── sms_service_spec.rb │ └── voice_service_spec.rb ├── mailers │ ├── notification_mailer_spec.rb │ └── previews │ │ └── notification_mailer_preview.rb ├── migrations │ ├── add_dimensions_to_attachments_spec.rb │ ├── add_reporting_relationship_to_message_spec.rb │ ├── add_type_message_spec.rb │ ├── cleanup_message_history_spec.rb │ ├── mark_inactive_messages_as_read_spec.rb │ ├── move_admin_user_to_users_spec.rb │ └── move_message_transfer_marker_to_marker_type_spec.rb ├── models │ ├── attachment_spec.rb │ ├── change_image_spec.rb │ ├── client_spec.rb │ ├── client_status_spec.rb │ ├── court_date_csv_spec.rb │ ├── court_reminder_spec.rb │ ├── department_spec.rb │ ├── highlight_blob_spec.rb │ ├── mass_message_spec.rb │ ├── message_spec.rb │ ├── report_spec.rb │ ├── reporting_relationship_spec.rb │ ├── survey_question_spec.rb │ ├── survey_response_link_spec.rb │ ├── survey_response_spec.rb │ ├── survey_spec.rb │ ├── text_message_spec.rb │ └── user_spec.rb ├── rails_helper.rb ├── requests │ ├── admin │ │ ├── client_spec.rb │ │ ├── court_date_csv_spec.rb │ │ ├── department_spec.rb │ │ ├── highlight_blob_spec.rb │ │ ├── import_csv_request_spec.rb │ │ ├── reporting_relationship_spec.rb │ │ └── user_spec.rb │ ├── clients_index_sorting_spec.rb │ ├── clients_request_spec.rb │ ├── help_spec.rb │ ├── layout_request_spec.rb │ ├── login_spec.rb │ ├── mass_messages_spec.rb │ ├── merge_reporting_relationships_spec.rb │ ├── messages_analytics_spec.rb │ ├── messages_spec.rb │ ├── reads_spec.rb │ ├── registrations_analytics_spec.rb │ ├── reporting_relationships_spec.rb │ ├── scheduled_messages_spec.rb │ ├── sessions_analytics_spec.rb │ ├── tracking_spec.rb │ ├── twilio_spec.rb │ ├── user_request_spec.rb │ └── welcomes_spec.rb ├── spec_helper.rb ├── support │ ├── active_job.rb │ ├── analytics_helper.rb │ ├── database_cleaner.rb │ ├── feature_helper.rb │ ├── match_html.rb │ ├── request_helper.rb │ ├── responsive_helper.rb │ ├── twilio_helper.rb │ └── wait_for_ajax.rb └── tasks │ ├── messages_spec.rb │ ├── reports_spec.rb │ └── utils_spec.rb └── vendor └── assets ├── javascripts ├── .keep ├── jquery.inview2.js ├── jquery.withinviewport.js ├── modernizr-custom.js ├── push.min.js ├── push.min.js.map ├── serviceWorker.min.js ├── serviceWorker.min.js.map ├── twemoji.js └── withinviewport.js └── stylesheets ├── .keep └── _normalize.scss /.env.example: -------------------------------------------------------------------------------- 1 | DEPLOYMENT=my-deployment 2 | DEPLOY_BASE_URL=https://my-deployment.example.com 3 | HELP_LINK=https://codeforamerica.github.io/clientcommfaq/ 4 | MIXPANEL_TOKEN=12345 5 | PORT=3000 6 | TIME_ZONE="Pacific Time (US & Canada)" 7 | TWILIO_ACCOUNT_SID= 8 | TWILIO_AUTH_TOKEN= 9 | TWILIO_PHONE_NUMBER= 10 | -------------------------------------------------------------------------------- /.flooignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # no bundler config 4 | /.bundle 5 | 6 | # no logfiles or tempfiles 7 | /log/* 8 | /tmp/* 9 | !/log/.keep 10 | !/tmp/.keep 11 | /coverage/* 12 | /results/* 13 | 14 | # JetBrains RubyMine 15 | .idea/* 16 | 17 | # no byebug command history file 18 | .byebug_history 19 | 20 | # no private environment variables 21 | .env 22 | .env-* 23 | 24 | # no private keys 25 | *.pem 26 | 27 | tags 28 | 29 | .floo 30 | -------------------------------------------------------------------------------- /.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 | .DS_Store 8 | 9 | # no bundler config 10 | /.bundle 11 | 12 | # no logfiles or tempfiles 13 | /log/* 14 | /tmp/* 15 | !/log/.keep 16 | !/tmp/.keep 17 | /coverage/* 18 | /results/* 19 | 20 | # Terraform 21 | .terraform/ 22 | *.tfstate 23 | *.tfstate.backup 24 | 25 | # JetBrains RubyMine 26 | .idea/* 27 | 28 | # no byebug command history file 29 | .byebug_history 30 | 31 | # no private environment variables 32 | .env 33 | .env-* 34 | 35 | # no private keys 36 | *.pem 37 | 38 | tags 39 | 40 | # Paperclip 41 | /public/system/* 42 | 43 | .floo 44 | 45 | # VIM 46 | *.swp 47 | 48 | # Node.js 49 | node_modules 50 | 51 | #Ansible 52 | *.retry 53 | -------------------------------------------------------------------------------- /.overcommit.yml: -------------------------------------------------------------------------------- 1 | # Use this file to configure the Overcommit hooks you wish to use. This will 2 | # extend the default configuration defined in: 3 | # https://github.com/brigade/overcommit/blob/master/config/default.yml 4 | # 5 | # At the topmost level of this YAML file is a key representing type of hook 6 | # being run (e.g. pre-commit, commit-msg, etc.). Within each type you can 7 | # customize each hook, such as whether to only run it on certain files (via 8 | # `include`), whether to only display output if it fails (via `quiet`), etc. 9 | # 10 | # For a complete list of hooks, see: 11 | # https://github.com/brigade/overcommit/tree/master/lib/overcommit/hook 12 | # 13 | # For a complete list of options that you can use to customize hooks, see: 14 | # https://github.com/brigade/overcommit#configuration 15 | CommitMsg: 16 | ALL: 17 | enabled: false 18 | 19 | PreCommit: 20 | AuthorEmail: 21 | enabled: false 22 | AuthorName: 23 | enabled: false 24 | 25 | RuboCop: 26 | enabled: true 27 | on_warn: fail # Treat all warnings as failures 28 | command: ['bundle', 'exec', 'rubocop'] 29 | flags: ['-R'] 30 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -r rspec_junit_formatter 4 | --format documentation 5 | --format RspecJunitFormatter 6 | --out results/rspec/junit.xml 7 | --exclude-pattern 'spec/migrations/**/*' 8 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.4.5 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Code for America 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec puma -t 5:5 -p ${PORT:-3000} -e ${RACK_ENV:-development} 2 | worker: rake jobs:work 3 | release: ./release-tasks.sh 4 | -------------------------------------------------------------------------------- /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/admin/client_status.rb: -------------------------------------------------------------------------------- 1 | ActiveAdmin.register ClientStatus do 2 | menu false 3 | permit_params :name, :followup_date, :icon_color, :department_id 4 | end 5 | -------------------------------------------------------------------------------- /app/admin/department.rb: -------------------------------------------------------------------------------- 1 | ActiveAdmin.register Department do 2 | menu priority: 3, parent: 'Manage' 3 | permit_params :user_id, :name, :phone_number, :unclaimed_response 4 | 5 | index do 6 | column :name 7 | column :phone_number 8 | column :unclaimed_user 9 | actions 10 | end 11 | 12 | form do |f| 13 | f.inputs do 14 | if resource.persisted? 15 | f.input :user_id, as: :select, collection: User.where(department: resource) 16 | end 17 | f.input :name 18 | f.input :phone_number 19 | f.input :unclaimed_response 20 | end 21 | f.actions 22 | end 23 | 24 | controller do 25 | def destroy 26 | if resource.users.any?(&:active) 27 | flash[:error] = 'Cannot delete a department with active users.' 28 | redirect_back(fallback_location: admin_departments_path) 29 | else 30 | super 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/admin/feature_flag.rb: -------------------------------------------------------------------------------- 1 | ActiveAdmin.register FeatureFlag do 2 | menu priority: 4, parent: 'Manage' 3 | 4 | permit_params :flag, :enabled 5 | actions :edit, :update, :index 6 | 7 | index do 8 | column :flag 9 | column :enabled 10 | actions 11 | end 12 | 13 | form do |f| 14 | f.inputs do 15 | f.input :flag 16 | f.input :enabled 17 | end 18 | f.actions 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/admin/highlight_blob.rb: -------------------------------------------------------------------------------- 1 | ActiveAdmin.register HighlightBlob do 2 | menu priority: 2, label: 'Highlight Box', parent: 'Manage' 3 | permit_params :text 4 | 5 | actions :all 6 | 7 | show title: 'Highlight Box' do 8 | panel 'Highlight Contents' do 9 | attributes_table_for highlight_blob do 10 | row(:text) { CGI.escapeHTML(highlight_blob.text) } 11 | end 12 | end 13 | end 14 | 15 | form do |f| 16 | f.inputs 'Text' do 17 | f.input :text 18 | end 19 | 20 | f.actions 21 | end 22 | 23 | controller do 24 | def index 25 | if HighlightBlob.first.present? 26 | redirect_to admin_highlight_blob_path(HighlightBlob.first) 27 | else 28 | redirect_to new_admin_highlight_blob_path 29 | end 30 | end 31 | 32 | def edit 33 | @page_title = 'Edit Highlight Box' 34 | super 35 | end 36 | 37 | def new 38 | @page_title = 'Create Highlight Box' 39 | super 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /app/admin/reports.rb: -------------------------------------------------------------------------------- 1 | ActiveAdmin.register Report do 2 | menu false 3 | permit_params :department_id, :email 4 | index do 5 | column :id 6 | column :email 7 | column :department 8 | actions 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../javascripts .js 3 | //= link_directory ../stylesheets .css 4 | -------------------------------------------------------------------------------- /app/assets/fonts/icomoon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/fonts/icomoon.eot -------------------------------------------------------------------------------- /app/assets/fonts/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/fonts/icomoon.ttf -------------------------------------------------------------------------------- /app/assets/fonts/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/fonts/icomoon.woff -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/.keep -------------------------------------------------------------------------------- /app/assets/images/emojis/1f35a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/emojis/1f35a.png -------------------------------------------------------------------------------- /app/assets/images/emojis/1f373.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/emojis/1f373.png -------------------------------------------------------------------------------- /app/assets/images/emojis/1f392.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/emojis/1f392.png -------------------------------------------------------------------------------- /app/assets/images/emojis/1f3e0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/emojis/1f3e0.png -------------------------------------------------------------------------------- /app/assets/images/emojis/1f447.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/emojis/1f447.png -------------------------------------------------------------------------------- /app/assets/images/emojis/1f44c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/emojis/1f44c.png -------------------------------------------------------------------------------- /app/assets/images/emojis/1f464.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/emojis/1f464.png -------------------------------------------------------------------------------- /app/assets/images/emojis/1f4a1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/emojis/1f4a1.png -------------------------------------------------------------------------------- /app/assets/images/emojis/1f4b0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/emojis/1f4b0.png -------------------------------------------------------------------------------- /app/assets/images/emojis/1f4b2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/emojis/1f4b2.png -------------------------------------------------------------------------------- /app/assets/images/emojis/1f4b3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/emojis/1f4b3.png -------------------------------------------------------------------------------- /app/assets/images/emojis/1f4b5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/emojis/1f4b5.png -------------------------------------------------------------------------------- /app/assets/images/emojis/1f4bb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/emojis/1f4bb.png -------------------------------------------------------------------------------- /app/assets/images/emojis/1f4c4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/emojis/1f4c4.png -------------------------------------------------------------------------------- /app/assets/images/emojis/1f4cb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/emojis/1f4cb.png -------------------------------------------------------------------------------- /app/assets/images/emojis/1f4dd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/emojis/1f4dd.png -------------------------------------------------------------------------------- /app/assets/images/emojis/1f4eb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/emojis/1f4eb.png -------------------------------------------------------------------------------- /app/assets/images/emojis/1f4f2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/emojis/1f4f2.png -------------------------------------------------------------------------------- /app/assets/images/emojis/1f512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/emojis/1f512.png -------------------------------------------------------------------------------- /app/assets/images/emojis/1f600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/emojis/1f600.png -------------------------------------------------------------------------------- /app/assets/images/emojis/1f60a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/emojis/1f60a.png -------------------------------------------------------------------------------- /app/assets/images/emojis/1f610.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/emojis/1f610.png -------------------------------------------------------------------------------- /app/assets/images/emojis/1f641.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/emojis/1f641.png -------------------------------------------------------------------------------- /app/assets/images/emojis/1f6e1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/emojis/1f6e1.png -------------------------------------------------------------------------------- /app/assets/images/emojis/1f914.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/emojis/1f914.png -------------------------------------------------------------------------------- /app/assets/images/emojis/1f951.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/emojis/1f951.png -------------------------------------------------------------------------------- /app/assets/images/emojis/1f955.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/emojis/1f955.png -------------------------------------------------------------------------------- /app/assets/images/emojis/1f957.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/emojis/1f957.png -------------------------------------------------------------------------------- /app/assets/images/emojis/1f958.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/emojis/1f958.png -------------------------------------------------------------------------------- /app/assets/images/emojis/260e.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/emojis/260e.png -------------------------------------------------------------------------------- /app/assets/images/emojis/2705.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/emojis/2705.png -------------------------------------------------------------------------------- /app/assets/images/emojis/270a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/emojis/270a.png -------------------------------------------------------------------------------- /app/assets/images/emojis/270f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/emojis/270f.png -------------------------------------------------------------------------------- /app/assets/images/emojis/2712.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/emojis/2712.png -------------------------------------------------------------------------------- /app/assets/images/empty_templates.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/empty_templates.png -------------------------------------------------------------------------------- /app/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/favicon.ico -------------------------------------------------------------------------------- /app/assets/images/favicon.read.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/favicon.read.png -------------------------------------------------------------------------------- /app/assets/images/favicon.unread.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/favicon.unread.png -------------------------------------------------------------------------------- /app/assets/images/handshake.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/handshake.png -------------------------------------------------------------------------------- /app/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/images/logo.png -------------------------------------------------------------------------------- /app/assets/images/mailbox.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/open-mailbox-with-raised-flag.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/javascripts/active_admin.js: -------------------------------------------------------------------------------- 1 | //= require active_admin/base 2 | -------------------------------------------------------------------------------- /app/assets/javascripts/cable.js: -------------------------------------------------------------------------------- 1 | // Action Cable provides the framework to deal with WebSockets in Rails. 2 | // You can generate new channels where WebSocket features live using the rails generate channel command. 3 | // 4 | //= require action_cable 5 | //= require_self 6 | //= require channels/clients 7 | //= require channels/notifications 8 | //= require channels/scheduled_messages 9 | //= require channels/events 10 | 11 | (function() { 12 | this.App || (this.App = {}); 13 | 14 | App.cable = ActionCable.createConsumer(); 15 | 16 | }).call(this); 17 | -------------------------------------------------------------------------------- /app/assets/javascripts/channels/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/javascripts/channels/.keep -------------------------------------------------------------------------------- /app/assets/javascripts/channels/clients.js: -------------------------------------------------------------------------------- 1 | var Clients = { 2 | init: function () { 3 | this.$clientList = $('#client-list'); 4 | }, 5 | refreshClientList: function () { 6 | $.ajax({ 7 | type: "GET", 8 | dataType: "script" 9 | }); 10 | } 11 | }; 12 | 13 | $(document).ready(function () { 14 | Clients.init(); 15 | 16 | // only subscribe if we've got a client list (e.g. clients index page) 17 | if (Clients.$clientList.length === 0) { 18 | return; 19 | } 20 | 21 | if ($("meta[name='current-user']").length > 0) { 22 | App.messages = App.cable.subscriptions.create( 23 | { channel: 'ClientsChannel' }, 24 | { 25 | received: function() { 26 | Clients.refreshClientList(); 27 | } 28 | } 29 | ); 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /app/assets/javascripts/channels/events.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | if ($("meta[name='current-user']").length > 0) { 3 | App.events = App.cable.subscriptions.create( 4 | { channel: 'EventsChannel' }, 5 | { 6 | received: function(event) { 7 | $(window).trigger(event.type + '-event', [event.data]); 8 | } 9 | } 10 | ); 11 | } 12 | Push.Permission.request(function() {}, function() {}); 13 | }); 14 | -------------------------------------------------------------------------------- /app/assets/javascripts/channels/scheduled_messages.js: -------------------------------------------------------------------------------- 1 | var ScheduledMessages = { 2 | updateLink: function(link_html, count) { 3 | var linkElement = $('.scheduled-messages-bar'); 4 | if (count > 0) { 5 | linkElement.replaceWith(link_html); 6 | } else { 7 | linkElement.remove(); 8 | } 9 | } 10 | }; 11 | 12 | $(document).ready(function() { 13 | var clientId = $('#message-list').data('client-id'); 14 | 15 | // only subscribe if we're on a message page 16 | if (!clientId) { 17 | return; 18 | } 19 | 20 | if ($("meta[name='current-user']").length > 0) { 21 | App.scheduledMessages = App.cable.subscriptions.create( 22 | { channel: 'ScheduledMessagesChannel', client_id: clientId }, 23 | { 24 | received: function(data) { 25 | ScheduledMessages.updateLink(data.link_html, data.count); 26 | } 27 | } 28 | ); 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /app/assets/javascripts/clients.js: -------------------------------------------------------------------------------- 1 | //= require client_search 2 | 3 | var revealer = (function() { 4 | var rv = { 5 | init: function() { 6 | $('.reveal').each(function(index, revealer) { 7 | var self = revealer; 8 | $(self).addClass('is-hidden'); 9 | $(self).find('.reveal__link').click(function(e) { 10 | e.preventDefault(); 11 | $(self).toggleClass('is-hidden'); 12 | }); 13 | }); 14 | } 15 | } 16 | return { 17 | init: rv.init 18 | } 19 | })(); 20 | 21 | $(document).ready(function() { 22 | revealer.init(); 23 | }); 24 | -------------------------------------------------------------------------------- /app/assets/javascripts/clients/new.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | 3 | ///////////////////// 4 | // NEXT COURT DATE // 5 | ///////////////////// 6 | 7 | initializeDatepicker("#client_next_court_date_at"); 8 | 9 | }); 10 | -------------------------------------------------------------------------------- /app/assets/javascripts/errors.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/assets/javascripts/errors.js -------------------------------------------------------------------------------- /app/assets/javascripts/scheduled_messages.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){ 2 | $('#scheduled-list-modal').modal(); 3 | 4 | $('#scheduled-list-modal').on('hidden.bs.modal', function(e) { 5 | e.preventDefault(); 6 | window.location = $('.close').attr('href'); 7 | }); 8 | 9 | characterCount($('.send-later-input')); 10 | }); 11 | -------------------------------------------------------------------------------- /app/assets/javascripts/twemoji-init.js: -------------------------------------------------------------------------------- 1 | function replaceEmoji(element) { 2 | if (!Modernizr.emoji) { 3 | twemoji.parse(element) 4 | } 5 | } 6 | 7 | $(document).ready(function () { 8 | var messageList = document.getElementById('message-list') 9 | if (messageList) { replaceEmoji(messageList) } 10 | }) 11 | -------------------------------------------------------------------------------- /app/assets/stylesheets/_ie-hacks.scss: -------------------------------------------------------------------------------- 1 | @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { 2 | .select { 3 | &:before { 4 | display: none; 5 | } 6 | } 7 | .select__element { 8 | padding-right: .7em; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/assets/stylesheets/_shame.scss: -------------------------------------------------------------------------------- 1 | .inline-block { 2 | display: inline-block; 3 | } 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/atoms/_base.scss: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | 6 | html { 7 | position: relative; 8 | background-color: $color-background; 9 | font-size: $em-base; 10 | -webkit-font-smoothing: antialiased; 11 | min-height: 100%; 12 | } 13 | 14 | body { 15 | font-size: $font-size-normal; 16 | font-family: $font-system; 17 | line-height: $line-height-normal; 18 | } 19 | 20 | .page-wrapper { 21 | overflow: hidden; 22 | padding-left: $site-margins/2; 23 | padding-right: $site-margins/2; 24 | @include media($tablet-up) { 25 | padding-left: $site-margins; 26 | padding-right: $site-margins; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/assets/stylesheets/atoms/_form-widths.scss: -------------------------------------------------------------------------------- 1 | .form-width--two-digit { 2 | max-width: $width-form-two-digit; 3 | } 4 | 5 | .form-width--four-digit { 6 | max-width: $width-form-four-digit; 7 | } 8 | 9 | .form-width--short { 10 | max-width: $width-form-short; 11 | } 12 | 13 | .form-width--med { 14 | max-width: $width-form-med; 15 | } 16 | 17 | .form-width--mid { 18 | max-width: $width-form-mid; 19 | } 20 | 21 | .form-width--long { 22 | max-width: $width-form-long; 23 | } 24 | 25 | .form-width--casenumber { 26 | max-width: $width-form-casenumber; 27 | } 28 | 29 | .form-width--date { 30 | max-width: $width-form-date; 31 | } 32 | 33 | .form-width--name { 34 | max-width: $width-form-name; 35 | } 36 | 37 | .form-width--phone { 38 | max-width: $width-form-phone; 39 | } 40 | 41 | .form-width--ssn { 42 | max-width: $width-form-ssn; 43 | } 44 | 45 | .form-width--zip { 46 | max-width: $width-form-zip; 47 | } 48 | 49 | .form-width--searchbar { 50 | max-width: $width-form-searchbar; 51 | } 52 | -------------------------------------------------------------------------------- /app/assets/stylesheets/atoms/_icon_mods.scss: -------------------------------------------------------------------------------- 1 | [class^="icon-"].orange:before, 2 | [class*=" icon-"].orange:before { 3 | color: $color-orange; 4 | } 5 | -------------------------------------------------------------------------------- /app/assets/stylesheets/atoms/_images.scss: -------------------------------------------------------------------------------- 1 | img { 2 | max-width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/atoms/_labels.scss: -------------------------------------------------------------------------------- 1 | .label { 2 | display: inline-block; 3 | font-size: $font-size-smallest; 4 | background-color: $color-grey; 5 | color: #fff; 6 | font-weight: 500; 7 | border-radius: $border-radius; 8 | text-transform: uppercase; 9 | padding: 0 .5em; 10 | line-height: 1.4em; 11 | position: relative; 12 | top: -.1em; 13 | margin-left: .2em; 14 | } 15 | 16 | .label--magenta { 17 | background-color: $color-magenta; 18 | } 19 | 20 | .label--teal { 21 | background-color: $color-teal; 22 | } 23 | 24 | .label--green { 25 | background-color: $color-green; 26 | } 27 | 28 | .label--orange { 29 | background-color: $color-orange; 30 | } 31 | 32 | .label--red { 33 | background-color: $color-red; 34 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/email.scss: -------------------------------------------------------------------------------- 1 | @import 'mailers/mailer'; 2 | -------------------------------------------------------------------------------- /app/assets/stylesheets/molecules/_incrementer.scss: -------------------------------------------------------------------------------- 1 | .incrementer { 2 | display: table; 3 | border-collapse: separate; 4 | border-spacing: 1em; 5 | margin-bottom: .5em; 6 | .text-input { 7 | @include appearance(none); 8 | display: table-cell; 9 | text-align: center; 10 | margin-bottom: 0; 11 | padding: { 12 | left: 1em; 13 | right: 1em; 14 | } 15 | &::-webkit-inner-spin-button { 16 | display: none; 17 | } 18 | } 19 | } 20 | 21 | .incrementer__subtract, .incrementer__add { 22 | @include transition($animation-fast background-color); 23 | @include user-select(none); 24 | cursor: pointer; 25 | color: $color-teal; 26 | display: table-cell; 27 | width: 1%; 28 | background-color: $color-light-grey; 29 | font-size: $font-size-h3; 30 | font-weight: 500; 31 | padding: .5em 0; 32 | width: 2.3em; 33 | text-align: center; 34 | border-radius: $border-radius; 35 | text-decoration: none; 36 | &:hover { 37 | background-color: tint($color-light-grey, 30%); 38 | } 39 | &:active { 40 | background-color: shade($color-light-grey, 10%); 41 | } 42 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/molecules/_progress-indicator.scss: -------------------------------------------------------------------------------- 1 | .progress-indicator { 2 | width: 10em; 3 | margin: 2em auto; 4 | text-align: center; 5 | position: relative; 6 | &:before { 7 | content: ''; 8 | display: block; 9 | position: absolute; 10 | top: 0; 11 | height: 6px; 12 | left: 0; 13 | right: 0; 14 | border-radius: 3px; 15 | background-color: $color-light-grey; 16 | z-index: -1; 17 | border: 1px solid $color-tan; 18 | } 19 | } 20 | 21 | .progress-indicator__bar { 22 | height: 6px; 23 | min-width: 6px; 24 | border-radius: 3px; 25 | background-color: $color-teal; 26 | } 27 | 28 | .progress-indicator__percentage { 29 | margin-top: .5em; 30 | font-size: $font-size-small; 31 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/molecules/_reveal.scss: -------------------------------------------------------------------------------- 1 | .reveal__link { 2 | display: inline-block; 3 | text-decoration: none; 4 | color: $color-darkest-grey; 5 | background-color: rgba($color-teal, .08); 6 | border-radius: 10px; 7 | padding: .2em .5em .3em .7em; 8 | 9 | &:hover { 10 | color: $color-teal; 11 | } 12 | 13 | &:hover, &:link { 14 | text-decoration: none; 15 | } 16 | 17 | &:after { 18 | position: relative; 19 | top: .1em; 20 | font-family: $font-icon; 21 | content: '\a0\e313'; 22 | } 23 | } 24 | 25 | .reveal__content { 26 | margin-top: 1em; 27 | background-color: $color-background; 28 | border: 1px solid $color-light-grey; 29 | border-radius: 10px; 30 | margin-bottom: 2em; 31 | } 32 | 33 | .reveal { 34 | &.is-hidden { 35 | .reveal__link { 36 | &:after { 37 | content: '\a0\e315'; 38 | } 39 | } 40 | .reveal__content { 41 | display: none; 42 | } 43 | } 44 | 45 | .notice { 46 | background-color: rgba($color-teal, .05); 47 | margin-bottom: 0; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/assets/stylesheets/molecules/_scroller.scss: -------------------------------------------------------------------------------- 1 | @include keyframes('scroller') { 2 | 0% { 3 | opacity: 0; 4 | } 5 | 6 | 100% { 7 | opacity: 1; 8 | } 9 | 10 | } 11 | 12 | .scroller { 13 | @include animation(scroller 2s 1); 14 | position: absolute; 15 | left: 50%; 16 | bottom: 1em; 17 | margin-left: -1em; 18 | display: block; 19 | width: 2em; 20 | height: 2em; 21 | color: $color-darkest-grey; 22 | text-decoration: none; 23 | &:before { 24 | font-family: $font-icon; 25 | font-size: $font-size-h2; 26 | display: block; 27 | content: '\e313'; 28 | text-align: center; 29 | line-height: 1.5em; 30 | } 31 | } 32 | 33 | .scroller--light { 34 | color: #FFFFFF; 35 | &:hover { 36 | color: $color-magenta; 37 | } 38 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/molecules/_steps.scss: -------------------------------------------------------------------------------- 1 | .steps { 2 | position: relative; 3 | 4 | @include media($tablet-up) { 5 | &:before { 6 | content: ''; 7 | display: block; 8 | position: absolute; 9 | top: 25px; 10 | height: 3px; 11 | width: 80%; 12 | margin-left: 10%; 13 | background-color: $color-light-grey; 14 | z-index: 0; 15 | } 16 | } 17 | 18 | .emoji, .illustration { 19 | position: relative; 20 | background-color: #fff; 21 | margin-bottom: 1em; 22 | &:before { 23 | content: ''; 24 | z-index: 0; 25 | display: block; 26 | position: absolute; 27 | border: 20px solid #fff; 28 | top: -20px; 29 | right: -20px; 30 | bottom: -20px; 31 | left: -20px; 32 | } 33 | } 34 | 35 | .grid__item { 36 | margin-bottom: 2em; 37 | @include media($tablet-up) { 38 | margin-bottom: 0; 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/molecules/_summary-table.scss: -------------------------------------------------------------------------------- 1 | .summary-table { 2 | text-align: center; 3 | .grid__item { 4 | margin-bottom: 1em; 5 | } 6 | } 7 | 8 | .summary-table--left { 9 | text-align: left; 10 | } 11 | 12 | .summary-table__label { 13 | @extend .text--help; 14 | margin-bottom: 0; 15 | } 16 | 17 | .summary-table__value { 18 | @extend .text--pullquote; 19 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/molecules/_tabs.scss: -------------------------------------------------------------------------------- 1 | .tab-bar { 2 | @include clearfix(); 3 | @include full-bleed(); 4 | border-bottom: 2px solid $color-tan; 5 | margin-top: 1em; 6 | font-size: $font-size-small; 7 | } 8 | 9 | .tab-bar__tab { 10 | position: relative; 11 | display: block; 12 | float: left; 13 | margin-right: .5em; 14 | margin-bottom: -2px; 15 | border: 2px solid $color-tan; 16 | background-color: rgba($color-tan, .25); 17 | padding: .5em 1.5em; 18 | color: $color-grey; 19 | text-decoration: none; 20 | min-width: 8em; 21 | text-align: center; 22 | 23 | &:hover { 24 | background-color: rgba($color-teal, .1); 25 | color: $color-grey; 26 | } 27 | 28 | &.is-selected { 29 | z-index: 2; 30 | color: #000; 31 | font-weight: 600; 32 | background-color: $color-background; 33 | &:before { 34 | content: ''; 35 | position: absolute; 36 | display: block; 37 | left: 0; 38 | right: 0; 39 | bottom: -2px; 40 | height: 2px; 41 | background-color: $color-background; 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/molecules/_toolbar-responsive.scss: -------------------------------------------------------------------------------- 1 | .toolbar-responsive { 2 | @include clearfix(); 3 | max-width: $site-max-width; 4 | margin: { 5 | left: auto; 6 | right: auto; 7 | } 8 | } 9 | 10 | .toolbar--centered-responsive { 11 | display: inline-block; 12 | } 13 | 14 | .toolbar__item-responsive { 15 | float: left; 16 | margin-right: 1em; 17 | } 18 | 19 | .toolbar__left-responsive { 20 | float: left; 21 | margin-right: .5em; 22 | } 23 | 24 | .toolbar__right-responsive { 25 | float: left; 26 | margin-left: 0; 27 | @include media($mobile-up) { 28 | float: right; 29 | } 30 | .toolbar__item { 31 | margin-left: 0; 32 | @include media($mobile-up) { 33 | margin-left: .5em; 34 | display: inline-block; 35 | float: left; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/assets/stylesheets/molecules/_toolbar.scss: -------------------------------------------------------------------------------- 1 | .toolbar { 2 | @include clearfix(); 3 | max-width: $site-max-width; 4 | margin: { 5 | left: auto; 6 | right: auto; 7 | } 8 | } 9 | 10 | .toolbar--centered { 11 | display: inline-block; 12 | } 13 | 14 | .toolbar__item { 15 | float: left; 16 | margin-right: 1em; 17 | } 18 | 19 | .toolbar__left { 20 | float: left; 21 | margin-right: .5em; 22 | } 23 | 24 | .toolbar__right { 25 | float: right; 26 | margin-left: .5em; 27 | .toolbar__item { 28 | margin-right: 0; 29 | margin-left: 1em; 30 | display: inline-block; 31 | float: left; 32 | } 33 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/organisms/_demo-banner.scss: -------------------------------------------------------------------------------- 1 | .demo-banner { 2 | @include full-bleed(); 3 | position: relative; 4 | z-index: 2; 5 | background-color: $color-red; 6 | padding: .5em; 7 | text-align: center; 8 | font-size: $font-size-small; 9 | color: #FFFFFF; 10 | } 11 | 12 | -------------------------------------------------------------------------------- /app/assets/stylesheets/organisms/_document-preview.scss: -------------------------------------------------------------------------------- 1 | .document-previewer { 2 | display: inline-block; 3 | margin-bottom: 1em; 4 | 5 | .document-preview { 6 | display: inline-block; 7 | padding: 5px; 8 | margin-bottom: 0.5em; 9 | max-width: 100px; 10 | margin-right: 1em; 11 | text-align: center; 12 | img { 13 | display: block; 14 | background-color: $color-grey; 15 | } 16 | } 17 | } 18 | 19 | .document-upload { 20 | .form { 21 | &__documentuploader { 22 | border-width: 1px; 23 | border-color: $color-tan; 24 | border-style: solid; 25 | margin-bottom: 0.5em; 26 | padding: 1em; 27 | text-align: center; 28 | width: 100%; 29 | line-height: normal; 30 | 31 | &:hover { 32 | background-color: $color-light-grey; 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/assets/stylesheets/organisms/_legal.scss: -------------------------------------------------------------------------------- 1 | .legal { 2 | margin-top: -1em; 3 | margin-bottom: 2em; 4 | max-width: 30em; 5 | margin-left: auto; 6 | margin-right: auto; 7 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/organisms/_main-footer.scss: -------------------------------------------------------------------------------- 1 | body { 2 | margin-bottom: $footer-height*1.5; 3 | @include media($tablet-up) { 4 | margin-bottom: $footer-height; 5 | } 6 | 7 | } 8 | 9 | .main-footer { 10 | position: absolute; 11 | bottom: 0; 12 | left: 0; 13 | right: 0; 14 | height: $footer-height*1.5; 15 | color: $color-grey; 16 | background-color: $color-darkest-grey; 17 | padding: 2em $site-margins/2; 18 | font-weight: 600; 19 | 20 | @include media($tablet-up) { 21 | padding: 2em $site-margins; 22 | height: $footer-height; 23 | } 24 | } 25 | 26 | .main-footer__legal { 27 | font-size: $font-size-small; 28 | p { 29 | @include clearfix(); 30 | margin-bottom: 0em; 31 | } 32 | } 33 | 34 | .main-footer__cfa-logo { 35 | margin-top: 2em; 36 | 37 | @include media($tablet-up) { 38 | margin-top: 1em; 39 | float: right; 40 | } 41 | } 42 | 43 | .main-footer .select:before { 44 | color: $color-grey; 45 | } 46 | 47 | .main-footer .select__element { 48 | border-radius: 0; 49 | } 50 | 51 | .main-footer .select__element:focus { 52 | border-color: $color-grey; 53 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/organisms/_pagination.scss: -------------------------------------------------------------------------------- 1 | .pagination { 2 | @include clearfix; 3 | padding: 2em 0; 4 | 5 | .text--help { 6 | margin-top: .3em; 7 | } 8 | 9 | .button { 10 | min-width: 2.5em; 11 | text-align: center; 12 | } 13 | 14 | .pagination__selected { 15 | @extend .button--primary; 16 | } 17 | 18 | .pagination__ellipsis { 19 | display: inline-block; 20 | width: 2em; 21 | text-align: center; 22 | } 23 | 24 | @include media($tablet-up) { 25 | 26 | .pagination__info { 27 | float: left; 28 | } 29 | 30 | .pagination__buttons { 31 | float: right; 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/organisms/_statistic-card.scss: -------------------------------------------------------------------------------- 1 | .statistic-card { 2 | @extend .card; 3 | } 4 | 5 | .statistic-card__label { 6 | @extend .text--help; 7 | margin-bottom: .5em; 8 | } 9 | 10 | .statistic-card__number { 11 | @extend .h1; 12 | margin-top: 0; 13 | } 14 | -------------------------------------------------------------------------------- /app/assets/stylesheets/templates/_about.scss: -------------------------------------------------------------------------------- 1 | .template--about { 2 | .main-header { 3 | background-color: #FFFFFF; 4 | border-bottom: 0; 5 | } 6 | 7 | .slab--header { 8 | background-color: #FFFFFF; 9 | padding: { 10 | top: 3em; 11 | bottom: 3em; 12 | } 13 | 14 | @include media($tablet-up) { 15 | padding: { 16 | top: 4em; 17 | bottom: 4em; 18 | } 19 | } 20 | 21 | .button--primary { 22 | margin: { 23 | left: auto; 24 | right: auto; 25 | } 26 | } 27 | } 28 | 29 | h2 { 30 | text-align: center; 31 | font-size: $font-size-h1; 32 | } 33 | 34 | .partner-logo { 35 | display: block; 36 | float: left; 37 | width: 30%; 38 | margin: 5% 10%; 39 | 40 | @include media($tablet-up) { 41 | width: 15%; 42 | margin: 5%; 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/templates/_dashboard.scss: -------------------------------------------------------------------------------- 1 | .template--dashboard { 2 | font-size: $font-size-small; 3 | 4 | .page-wrapper, .main-footer { 5 | overflow: auto; 6 | min-width: $site-max-width; 7 | } 8 | 9 | .dashboard-main--empty { 10 | margin-top: 10em; 11 | text-align: center; 12 | } 13 | 14 | .grid, .toolbar { 15 | max-width: none; 16 | } 17 | 18 | .main-header { 19 | border-bottom: 0; 20 | } 21 | 22 | .dashboard-toolbar { 23 | padding: 3em 0; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/assets/stylesheets/templates/_error.scss: -------------------------------------------------------------------------------- 1 | .template--error { 2 | margin-top: 2em; 3 | margin-bottom: 2em; 4 | @include media($tablet-up) { 5 | margin-top: 4em; 6 | margin-bottom: 4em; 7 | } 8 | 9 | } 10 | 11 | .form-group--error-modal { 12 | @include full-bleed-modal(); 13 | .form-group--error:before { 14 | content: ''; 15 | display: block; 16 | position: absolute; 17 | top: -.5em; 18 | bottom: -.5em; 19 | left: 25px; 20 | width: 4px; 21 | background-color: pink; 22 | 23 | @include media($tablet-up) { 24 | width: 6px; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/assets/stylesheets/templates/_mass-messages.scss: -------------------------------------------------------------------------------- 1 | .template--mass-messages { 2 | .data-table { 3 | label { 4 | font-weight: normal; 5 | cursor: pointer; 6 | } 7 | } 8 | #schedule-later-form { 9 | display: none; 10 | } 11 | 12 | #schedule-later-buttons { 13 | display: none; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/assets/stylesheets/templates/_numbers.scss: -------------------------------------------------------------------------------- 1 | .template--numbers { 2 | .big-number { 3 | font-size: 10rem; 4 | line-height: 10rem; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /app/assets/stylesheets/templates/_question.scss: -------------------------------------------------------------------------------- 1 | .template--question { 2 | .form-card { 3 | margin-top: -2em; 4 | max-width: 72rem; 5 | 6 | 7 | @include media($tablet-up) { 8 | margin-top: 0; 9 | margin-bottom: 1em; 10 | margin: { 11 | left: auto; 12 | right: auto; 13 | } 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/templates/_static-page.scss: -------------------------------------------------------------------------------- 1 | .template--static-page { 2 | .slab--header { 3 | background-color: #FFFFFF; 4 | padding: { 5 | top: 6em; 6 | bottom: 6em; 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/templates/_welcome-form.scss: -------------------------------------------------------------------------------- 1 | .template--welcome-form { 2 | 3 | b { 4 | color: $color-teal; 5 | } 6 | 7 | .textarea { 8 | height: 6em; 9 | margin-top: 1em; 10 | padding: .5em 1em 1em 1em; 11 | } 12 | 13 | .welcome-form { 14 | max-width: 690px; 15 | margin-top: 2rem; 16 | margin-bottom: 2em; 17 | 18 | @include media($mobile-up) { 19 | margin-top: 10rem; 20 | } 21 | } 22 | 23 | .media-box { 24 | .media--small { 25 | margin-right: 0.8em; 26 | 27 | img { 28 | max-height: $width-image-small; 29 | } 30 | } 31 | } 32 | 33 | .reveal__content, .reveal__link { 34 | margin-bottom: 0.5em; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /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 | identified_by :current_user 4 | 5 | def connect 6 | self.current_user = find_verified_user 7 | logger.add_tags 'ActionCable', current_user.email 8 | end 9 | 10 | protected 11 | 12 | def find_verified_user 13 | # this checks whether a user is authenticated with devise 14 | if (verified_user = env['warden'].user) 15 | verified_user 16 | else 17 | reject_unauthorized_connection 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/channels/clients_channel.rb: -------------------------------------------------------------------------------- 1 | class ClientsChannel < ApplicationCable::Channel 2 | def subscribed 3 | stream_from "clients_#{current_user.id}" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/channels/events_channel.rb: -------------------------------------------------------------------------------- 1 | class EventsChannel < ApplicationCable::Channel 2 | def subscribed 3 | stream_from "events_#{current_user.id}" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/channels/messages_channel.rb: -------------------------------------------------------------------------------- 1 | class MessagesChannel < ApplicationCable::Channel 2 | def subscribed 3 | reject unless current_user.clients.pluck(:id).include? params[:client_id] 4 | stream_from "messages_#{current_user.id}_#{params[:client_id]}" 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/channels/notifications_channel.rb: -------------------------------------------------------------------------------- 1 | class NotificationsChannel < ApplicationCable::Channel 2 | def subscribed 3 | stream_from "notifications_#{current_user.id}" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/channels/scheduled_messages_channel.rb: -------------------------------------------------------------------------------- 1 | class ScheduledMessagesChannel < ApplicationCable::Channel 2 | def subscribed 3 | reject unless current_user.clients.pluck(:id).include? params[:client_id] 4 | stream_from "scheduled_messages_#{params[:client_id]}" 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | include AnalyticsHelper 3 | 4 | protect_from_forgery with: :exception 5 | before_action :set_visitor_id 6 | before_action :set_phone_number 7 | before_action :enable_intercom_launcher 8 | 9 | def access_denied(exception) 10 | redirect_to root_path, alert: exception.message 11 | end 12 | 13 | protected 14 | 15 | def after_sign_in_path_for(resource) 16 | if resource.is_a?(User) && resource.admin? && resource.clients.active.empty? 17 | admin_root_path 18 | else 19 | super 20 | end 21 | end 22 | 23 | private 24 | 25 | def enable_intercom_launcher 26 | IntercomRails.config.hide_default_launcher = false 27 | end 28 | 29 | # ANALYTICS 30 | 31 | def set_visitor_id 32 | session[:visitor_id] ||= SecureRandom.hex(4) 33 | end 34 | 35 | # COMMON 36 | 37 | def set_phone_number 38 | if current_user && (phone_number = current_user.department.phone_number) 39 | @clientcomm_phone_number ||= ::PhoneNumberParser.format_for_display(phone_number) 40 | else 41 | '' 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/controllers/errors_controller.rb: -------------------------------------------------------------------------------- 1 | class ErrorsController < ApplicationController 2 | def not_found 3 | request.format = 'html' 4 | 5 | @title = 'ClientComm.org | Page not found (404)' 6 | 7 | respond_to do |format| 8 | format.any { render status: :not_found } 9 | end 10 | end 11 | 12 | def internal_server_error 13 | @title = 'ClientComm | There has been a problem on our end (500)' 14 | 15 | render(status: :internal_server_error) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/controllers/file_previews_controller.rb: -------------------------------------------------------------------------------- 1 | class FilePreviewsController < ApplicationController 2 | # to use this, go to test.rb and add the line: 3 | # config.assets.debug = true 4 | # but, this'll make it very slow, so be sure to turn it off when 5 | # you don't need it 6 | def show 7 | render text: IO.read(params[:file]) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/controllers/help_controller.rb: -------------------------------------------------------------------------------- 1 | class HelpController < ApplicationController 2 | def index 3 | analytics_track( 4 | label: 'help_page_click' 5 | ) 6 | 7 | raise(ActionController::RoutingError, 'No Help Link Set') if ENV['HELP_LINK'].blank? 8 | redirect_to ENV['HELP_LINK'] 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/controllers/messages/reads_controller.rb: -------------------------------------------------------------------------------- 1 | module Messages 2 | class ReadsController < ApplicationController 3 | before_action :authenticate_user! 4 | skip_after_action :intercom_rails_auto_include 5 | 6 | def create 7 | message = current_user.messages.update(params[:message_id], read: message_params[:read]) 8 | if message.reporting_relationship.messages.unread.empty? 9 | message.reporting_relationship.update!(has_unread_messages: false) 10 | user = message.reporting_relationship.user 11 | user.set_has_unread_messages 12 | end 13 | head :no_content 14 | end 15 | 16 | private 17 | 18 | def message_params 19 | params.require(:message) 20 | .permit(:read) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/controllers/reporting_relationships/welcomes_controller.rb: -------------------------------------------------------------------------------- 1 | module ReportingRelationships 2 | class WelcomesController < ApplicationController 3 | before_action :authenticate_user! 4 | 5 | def new 6 | @reporting_relationship = ReportingRelationship.find params[:reporting_relationship_id] 7 | salutation = 'Good afternoon' 8 | salutation = 'Good morning' if Time.zone.now < Time.zone.now.noon 9 | @welcome_body = t( 10 | 'message.welcome', 11 | salutation: salutation, 12 | client_full_name: @reporting_relationship.client.full_name, 13 | user_last_name: @reporting_relationship.user.full_name.split(' ').last 14 | ) 15 | 16 | analytics_track( 17 | label: 'welcome_prompt_view', 18 | data: @reporting_relationship.client.analytics_tracker_data 19 | ) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/controllers/scheduled_messages_controller.rb: -------------------------------------------------------------------------------- 1 | class ScheduledMessagesController < ApplicationController 2 | before_action :authenticate_user! 3 | skip_after_action :intercom_rails_auto_include 4 | 5 | def index 6 | rr = current_user.reporting_relationships.find params[:reporting_relationship_id] 7 | @client = rr.client 8 | 9 | reporting_relationship = @client.reporting_relationship(user: current_user) 10 | 11 | analytics_track( 12 | label: 'client_scheduled_messages_view', 13 | data: @client.analytics_tracker_data.merge(reporting_relationship.analytics_tracker_data) 14 | ) 15 | 16 | # the list of past messages 17 | @messages = reporting_relationship.messages 18 | .where('send_at < ? OR send_at IS NULL', Time.zone.now) 19 | .order('created_at ASC') 20 | @messages.where(read: false).update(read: true) 21 | 22 | @messages_scheduled = reporting_relationship.messages.scheduled 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/controllers/tracking_events_controller.rb: -------------------------------------------------------------------------------- 1 | class TrackingEventsController < ApplicationController 2 | before_action :authenticate_user! 3 | skip_after_action :intercom_rails_auto_include 4 | 5 | def create 6 | track_data = params.key?('data') ? data : {} 7 | analytics_track(label: label, data: track_data) 8 | end 9 | 10 | private 11 | 12 | def label 13 | params.require(:label) 14 | end 15 | 16 | def data 17 | params.require(:data).permit! 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/controllers/users/confirmations_controller.rb: -------------------------------------------------------------------------------- 1 | module Users 2 | class ConfirmationsController < Devise::ConfirmationsController 3 | # GET /resource/confirmation/new 4 | # def new 5 | # super 6 | # end 7 | 8 | # POST /resource/confirmation 9 | # def create 10 | # super 11 | # end 12 | 13 | # GET /resource/confirmation?confirmation_token=abcdef 14 | # def show 15 | # super 16 | # end 17 | 18 | # protected 19 | 20 | # The path used after resending confirmation instructions. 21 | # def after_resending_confirmation_instructions_path_for(resource_name) 22 | # super(resource_name) 23 | # end 24 | 25 | # The path used after confirmation. 26 | # def after_confirmation_path_for(resource_name, resource) 27 | # super(resource_name, resource) 28 | # end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/controllers/users/omniauth_callbacks_controller.rb: -------------------------------------------------------------------------------- 1 | module Users 2 | class OmniauthCallbacksController < Devise::OmniauthCallbacksController 3 | # You should configure your model like this: 4 | # devise :omniauthable, omniauth_providers: [:twitter] 5 | 6 | # You should also create an action method in this controller like this: 7 | # def twitter 8 | # end 9 | 10 | # More info at: 11 | # https://github.com/plataformatec/devise#omniauth 12 | 13 | # GET|POST /resource/auth/twitter 14 | # def passthru 15 | # super 16 | # end 17 | 18 | # GET|POST /users/auth/twitter/callback 19 | # def failure 20 | # super 21 | # end 22 | 23 | # protected 24 | 25 | # The path used when OmniAuth fails 26 | # def after_omniauth_failure_path_for(scope) 27 | # super(scope) 28 | # end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/controllers/users/unlocks_controller.rb: -------------------------------------------------------------------------------- 1 | module Users 2 | class UnlocksController < Devise::UnlocksController 3 | # GET /resource/unlock/new 4 | # def new 5 | # super 6 | # end 7 | 8 | # POST /resource/unlock 9 | # def create 10 | # super 11 | # end 12 | 13 | # GET /resource/unlock?unlock_token=abcdef 14 | # def show 15 | # super 16 | # end 17 | 18 | # protected 19 | 20 | # The path used after sending unlock password instructions 21 | # def after_sending_unlock_instructions_path_for(resource) 22 | # super(resource) 23 | # end 24 | 25 | # The path used after unlocking the resource 26 | # def after_unlock_path_for(resource) 27 | # super(resource) 28 | # end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | def phone_number_display(phone_number) 3 | # format the passed phone number for display 4 | return nil if phone_number.blank? 5 | PhoneNumberParser.format_for_display(phone_number) 6 | end 7 | 8 | def message_inbound_or_outbound(message) 9 | # return a string indicating whether the message is inbound or outbound 10 | return Message::INBOUND if message.inbound 11 | Message::OUTBOUND 12 | end 13 | 14 | def feature_flag_for(flag) 15 | @flags ||= {} 16 | @flags[flag] = FeatureFlag.enabled?(flag) if @flags[flag].nil? 17 | @flags[flag] 18 | end 19 | 20 | def client_messages_status(rr) 21 | if rr.has_message_error 22 | Message::ERROR 23 | elsif rr.has_unread_messages 24 | Message::UNREAD 25 | else 26 | Message::READ 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | end 3 | -------------------------------------------------------------------------------- /app/jobs/dead_man_switch_job.rb: -------------------------------------------------------------------------------- 1 | require 'aws-sdk' 2 | 3 | class DeadManSwitchJob < ApplicationJob 4 | include ActionView::RecordIdentifier 5 | queue_as :dead_man_switch 6 | 7 | def perform 8 | CLOUD_WATCH.put_metric_data( 9 | namespace: ENV['DEPLOYMENT'], 10 | metric_data: [ 11 | { 12 | metric_name: 'DeadManSwitchRan', 13 | timestamp: Time.zone.now, 14 | value: 1.0, 15 | unit: 'None', 16 | storage_resolution: 1 17 | } 18 | ] 19 | ) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/jobs/incoming_message_job.rb: -------------------------------------------------------------------------------- 1 | class IncomingMessageJob < ApplicationJob 2 | include Rails.application.routes.url_helpers 3 | 4 | queue_as :high_priority 5 | 6 | def perform(params:) 7 | new_message = Message.create_from_twilio! params 8 | client_previously_active = new_message.reporting_relationship.active 9 | MessageHandler.handle_new_message(message: new_message) 10 | 11 | track( 12 | label: 'message_receive', 13 | user_id: new_message.reporting_relationship.user.id, 14 | data: new_message.analytics_tracker_data.merge(client_active: client_previously_active) 15 | ) 16 | end 17 | 18 | private 19 | 20 | def track(label:, user_id:, data: {}) 21 | tracking_data = { 22 | deploy: deploy_prefix 23 | }.merge(data) 24 | AnalyticsService.track( 25 | label: label, 26 | distinct_id: distinct_id(user_id), 27 | data: tracking_data 28 | ) 29 | end 30 | 31 | def deploy_prefix 32 | URI.parse(ENV['DEPLOY_BASE_URL']).hostname.split('.')[0..1].join('_') 33 | end 34 | 35 | def distinct_id(user_id) 36 | "#{deploy_prefix}-#{user_id}" 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /app/jobs/message_redaction_job.rb: -------------------------------------------------------------------------------- 1 | class MessageRedactionJob < ApplicationJob 2 | class TwilioNotFound < StandardError; end 3 | class TwilioNotComplete < StandardError; end 4 | 5 | queue_as :low_priority 6 | 7 | retry_on Twilio::REST::RestError, wait: ->(executions) { 240 * executions * executions } 8 | retry_on Faraday::ConnectionFailed, wait: :exponentially_longer 9 | 10 | def perform(message:) 11 | Rails.logger.warn "[MESSAGE_REDACTION_JOB] redacting message #{message.id}" 12 | SMSService.instance.redact_message(message: message) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/jobs/notification_broadcast_job.rb: -------------------------------------------------------------------------------- 1 | class NotificationBroadcastJob < ApplicationJob 2 | queue_as :default 3 | 4 | def perform(channel_id:, text:, link_to:, properties:) 5 | channel = "notifications_#{channel_id}" 6 | content = render_notification_partial(text, link_to) 7 | ActionCable.server.broadcast( 8 | channel, 9 | properties: properties, 10 | notification_html: content 11 | ) 12 | end 13 | 14 | def render_notification_partial(text, link_to) 15 | ClientsController.render( 16 | partial: 'layouts/flash', 17 | locals: { classes: ['flash'], body: text, link_to: link_to } 18 | ) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/jobs/scheduled_message_cron_job.rb: -------------------------------------------------------------------------------- 1 | class ScheduledMessageCronJob < ApplicationJob 2 | queue_as :scheduled_message_cron_job 3 | 4 | def perform 5 | send_messages = TextMessage.where(sent: false).where(inbound: false).where('send_at < ?', Time.zone.now + APP_CONFIG['scheduled_message_rate'].minutes) 6 | send_count = send_messages.count 7 | send_messages.find_each(&:send_message) 8 | CLOUD_WATCH.put_metric_data( 9 | namespace: ENV['DEPLOYMENT'], 10 | metric_data: [ 11 | { 12 | metric_name: 'MessagesScheduled', 13 | timestamp: Time.zone.now, 14 | value: send_count, 15 | unit: 'None', 16 | storage_resolution: 1 17 | } 18 | ] 19 | ) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/lib/active_admin_adapter.rb: -------------------------------------------------------------------------------- 1 | class ActiveAdminAdapter < ActiveAdmin::AuthorizationAdapter 2 | def authorized?(_action, _subject = nil) 3 | user.admin == true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/lib/date_parser.rb: -------------------------------------------------------------------------------- 1 | require 'timeliness' 2 | 3 | class DateParser 4 | def self.parse(date, time) 5 | Timeliness.parse("#{date} #{time}", format: 'mm/dd/yyyy h:nn_ampm', zone: ENV['TIME_ZONE']) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/lib/fuzzy_matcher.rb: -------------------------------------------------------------------------------- 1 | require 'fuzzystringmatch' 2 | 3 | class FuzzyMatcher 4 | def self.get_distance(string_a:, string_b:) 5 | FuzzyStringMatch::JaroWinkler.create(:native).getDistance(string_a, string_b) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/lib/list_maker.rb: -------------------------------------------------------------------------------- 1 | module ListMaker 2 | def self.transfer_users(user:) 3 | user.department.eligible_users.active.where.not(id: user.id).order(:full_name).pluck(:full_name, :id) 4 | end 5 | 6 | def self.merge_clients(user:, client:) 7 | user.reporting_relationships.includes(:client).active.where.not(client_id: client.id).order('clients.first_name, clients.last_name') 8 | .map do |rr| 9 | [ 10 | "#{rr.client.first_name&.strip} #{rr.client.last_name&.strip}", 11 | rr.client.id, 12 | { 'data-phone-number' => PhoneNumberParser.format_for_display(rr.client.phone_number), 13 | 'data-timestamp' => rr.timestamp } 14 | ] 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/lib/message_alert_builder.rb: -------------------------------------------------------------------------------- 1 | module MessageAlertBuilder 2 | extend ActionView::Helpers::TextHelper 3 | 4 | def self.build_alert(reporting_relationship:, reporting_relationship_path:, clients_path:) 5 | # return an alert appropriate for the state of unread messages 6 | unread_messages = reporting_relationship.user.messages.unread 7 | if unread_messages.empty? 8 | nil 9 | else 10 | lookup = unread_messages.group(:reporting_relationship).count 11 | if lookup.length == 1 12 | rr = lookup.keys.first 13 | message_count = lookup.values.first 14 | { 15 | text: "You have #{pluralize(message_count, 'unread message')} from #{rr.client.full_name}", 16 | link_to: reporting_relationship_path 17 | } 18 | else 19 | message_count = lookup.values.sum 20 | { 21 | text: "You have #{pluralize(message_count, 'unread message')}", 22 | link_to: clients_path 23 | } 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/lib/notification_sender.rb: -------------------------------------------------------------------------------- 1 | module NotificationSender 2 | def self.notify_users_of_changes(user:, client:) 3 | create_edit_markers_for_phone_number_change(user: user, client: client) 4 | notify_shared_users_of_changes(user: user, client: client) 5 | end 6 | 7 | def self.create_edit_markers_for_phone_number_change(user:, client:) 8 | return unless client.phone_number_previously_changed? 9 | 10 | Message.create_client_edit_markers( 11 | user: user, 12 | phone_number: client.phone_number, 13 | reporting_relationships: client.reporting_relationships.active, 14 | as_admin: false 15 | ) 16 | end 17 | 18 | def self.notify_shared_users_of_changes(user:, client:) 19 | other_active_relationships = client.reporting_relationships.active.where.not(user: user) 20 | 21 | other_active_relationships.each do |rr| 22 | NotificationMailer.client_edit_notification( 23 | notified_user: rr.user, 24 | editing_user: user, 25 | client: client, 26 | previous_changes: client.previous_changes.except(:updated_at, :next_court_date_at) 27 | ).deliver_later 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/lib/phone_number_parser.rb: -------------------------------------------------------------------------------- 1 | module PhoneNumberParser 2 | # NOTE: assumes US numbers 3 | 4 | def self.make_bare(phone_number) 5 | # return the phone number without country code or non-numeric characters 6 | stripped = phone_number.to_s.gsub(/\D+/, '') 7 | stripped[[-10, -1 * stripped.length].max..-1] 8 | end 9 | 10 | def self.format_for_display(phone_number) 11 | # format for display, like: "(243) 555-1212" 12 | bare = self.make_bare(phone_number) 13 | "(#{bare[0..2]}) #{bare[3..5]}-#{bare[6..-1]}" 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/lib/retrier.rb: -------------------------------------------------------------------------------- 1 | class Retrier 2 | def initialize(retries:, errors:) 3 | yield 4 | rescue *errors 5 | retries -= 1 6 | retry if retries.positive? 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/lib/voice_service.rb: -------------------------------------------------------------------------------- 1 | require 'singleton' 2 | 3 | class VoiceService 4 | def generate_text_response(message:) 5 | twiml = Twilio::TwiML::VoiceResponse.new 6 | twiml.say(message, voice: 'woman') 7 | twiml.to_s 8 | end 9 | 10 | def dial_number(phone_number:) 11 | twiml = Twilio::TwiML::VoiceResponse.new 12 | twiml.dial do |dial| 13 | dial.number(phone_number) 14 | end 15 | twiml.to_s 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'ClientComm ' 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /app/models/attachment.rb: -------------------------------------------------------------------------------- 1 | class Attachment < ApplicationRecord 2 | belongs_to :message 3 | 4 | has_attached_file :media 5 | validates_attachment_content_type :media, content_type: /video\/.*|audio\/.*|image\/.*|text\/.*|application\/pdf.*/ 6 | 7 | before_save :extract_dimensions 8 | serialize :dimensions, Array 9 | 10 | def update_media(url:) 11 | self.media = open(url, 12 | http_basic_authentication: [ENV['TWILIO_ACCOUNT_SID'], 13 | ENV['TWILIO_AUTH_TOKEN']]) 14 | end 15 | 16 | def image? 17 | media_content_type.match?(/image\/.*/) ? true : false 18 | end 19 | 20 | def extract_dimensions 21 | return unless image? 22 | tempfile = media.queued_for_write[:original] 23 | return if tempfile.nil? 24 | geometry = Paperclip::Geometry.from_file(tempfile) 25 | self.dimensions = [geometry.width.to_i, geometry.height.to_i] 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/models/change_image.rb: -------------------------------------------------------------------------------- 1 | class ChangeImage < ApplicationRecord 2 | has_attached_file :file 3 | belongs_to :user 4 | validates_attachment_content_type :file, content_type: %r{image\/.*} 5 | end 6 | -------------------------------------------------------------------------------- /app/models/client_edit_marker.rb: -------------------------------------------------------------------------------- 1 | class ClientEditMarker < Marker 2 | end 3 | -------------------------------------------------------------------------------- /app/models/client_status.rb: -------------------------------------------------------------------------------- 1 | class ClientStatus < ApplicationRecord 2 | belongs_to :department 3 | 4 | validates :name, :icon_color, :department, presence: true 5 | end 6 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/conversation_ends_marker.rb: -------------------------------------------------------------------------------- 1 | class ConversationEndsMarker < Marker 2 | private 3 | 4 | def set_marker 5 | self.inbound = true 6 | self.read = true 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/models/court_date_csv.rb: -------------------------------------------------------------------------------- 1 | class CourtDateCSV < ApplicationRecord 2 | has_attached_file :file 3 | belongs_to :user 4 | validates_attachment_content_type :file, content_type: %r{text\/.*} 5 | end 6 | -------------------------------------------------------------------------------- /app/models/court_reminder.rb: -------------------------------------------------------------------------------- 1 | class CourtReminder < TextMessage 2 | belongs_to :court_date_csv 3 | 4 | before_validation :set_court_reminder, on: :create 5 | # validates :court_date_csv, presence: true 6 | 7 | private 8 | 9 | def set_court_reminder 10 | self.inbound = false 11 | self.read = true 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/models/feature_flag.rb: -------------------------------------------------------------------------------- 1 | class FeatureFlag < ApplicationRecord 2 | validates :flag, presence: true, uniqueness: true 3 | validates :enabled, inclusion: { in: [true, false] } 4 | 5 | def self.enabled?(flag) 6 | feature = self.find_by(flag: flag) 7 | if feature 8 | feature.enabled 9 | else 10 | false 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/models/highlight_blob.rb: -------------------------------------------------------------------------------- 1 | class HighlightBlob < ApplicationRecord 2 | html_fragment :text, scrub: :prune 3 | end 4 | -------------------------------------------------------------------------------- /app/models/marker.rb: -------------------------------------------------------------------------------- 1 | class Marker < Message 2 | before_validation :set_marker, on: :create 3 | 4 | private 5 | 6 | def set_marker 7 | self.inbound = true 8 | self.send_at = Time.zone.now 9 | self.read = true 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/models/mass_message.rb: -------------------------------------------------------------------------------- 1 | class MassMessage 2 | include ActiveModel::Model 3 | # For validations see: 4 | # http://railscasts.com/episodes/219-active-model?view=asciicast 5 | 6 | validates :message, :reporting_relationships, presence: true 7 | 8 | attr_accessor :user, :message, :send_at, :reporting_relationships 9 | 10 | def initialize(attributes = {}) 11 | super 12 | @reporting_relationships ||= [] 13 | @reporting_relationships.map!(&:to_i) 14 | end 15 | 16 | def past_message? 17 | return false if send_at.nil? 18 | 19 | if send_at < time_buffer 20 | errors.add(:send_at, I18n.t('activerecord.errors.models.message.attributes.send_at.on_or_after')) 21 | 22 | true 23 | else 24 | false 25 | end 26 | end 27 | 28 | private 29 | 30 | def time_buffer 31 | Time.current - 5.minutes 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/models/merge_client.rb: -------------------------------------------------------------------------------- 1 | class MergeClient 2 | include ActiveModel::Model 3 | 4 | attr_accessor :full_name, :phone_number 5 | end 6 | -------------------------------------------------------------------------------- /app/models/merge_reporting_relationship.rb: -------------------------------------------------------------------------------- 1 | class MergeReportingRelationship 2 | include ActiveModel::Model 3 | 4 | attr_accessor :selected_client_id 5 | end 6 | -------------------------------------------------------------------------------- /app/models/merged_with_marker.rb: -------------------------------------------------------------------------------- 1 | class MergedWithMarker < Marker 2 | end 3 | -------------------------------------------------------------------------------- /app/models/report.rb: -------------------------------------------------------------------------------- 1 | class Report < ApplicationRecord 2 | belongs_to :department 3 | 4 | validates :department, presence: true 5 | validates :email, presence: true 6 | 7 | def users 8 | department.users.active 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/models/survey.rb: -------------------------------------------------------------------------------- 1 | class Survey < ApplicationRecord 2 | belongs_to :client 3 | belongs_to :user 4 | has_many :survey_response_links, dependent: :nullify 5 | has_many :survey_responses, through: :survey_response_links 6 | 7 | def questions 8 | survey_responses.map(&:survey_question).uniq 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/models/survey_question.rb: -------------------------------------------------------------------------------- 1 | class SurveyQuestion < ApplicationRecord 2 | has_many :survey_responses, dependent: :nullify 3 | end 4 | -------------------------------------------------------------------------------- /app/models/survey_response.rb: -------------------------------------------------------------------------------- 1 | class SurveyResponse < ApplicationRecord 2 | belongs_to :survey_question 3 | has_many :survey_response_links, dependent: :nullify 4 | 5 | scope :active, -> { where(active: true) } 6 | end 7 | -------------------------------------------------------------------------------- /app/models/survey_response_link.rb: -------------------------------------------------------------------------------- 1 | class SurveyResponseLink < ApplicationRecord 2 | belongs_to :survey 3 | belongs_to :survey_response 4 | end 5 | -------------------------------------------------------------------------------- /app/models/text_message.rb: -------------------------------------------------------------------------------- 1 | class TextMessage < Message 2 | validates :number_to, presence: true 3 | validates :number_from, presence: true 4 | 5 | before_validation :set_text_message, on: :create 6 | 7 | private 8 | 9 | def set_text_message 10 | return unless reporting_relationship 11 | 12 | if inbound 13 | self.number_from = reporting_relationship.client.phone_number 14 | self.number_to = reporting_relationship.user.department.phone_number 15 | else 16 | self.number_from = reporting_relationship.user.department.phone_number 17 | self.number_to = reporting_relationship.client.phone_number 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/models/transfer_marker.rb: -------------------------------------------------------------------------------- 1 | class TransferMarker < Marker 2 | end 3 | -------------------------------------------------------------------------------- /app/views/admin/users/disable.html.erb: -------------------------------------------------------------------------------- 1 |

Disabling an account means that this user will no longer be able to log in. You can re-enable their account at any 2 | time.

3 | <% if @clients_to_disable.any? %> 4 |
5 |

Warning: This user has active clients

6 |
7 |

8 | These clients will no longer be able to text this user. 9 | Incoming texts will be received by the active user most 10 | recently in contact with this client, or the unclaimed user for this department. 11 |

12 |
    13 | <% @clients_to_disable.each do |client| %> 14 |
  • <%= client.full_name %>
  • 15 | <% end %> 16 |
17 |
18 |
19 | <% end %> 20 | <%= link_to 'Disable account', disable_confirm_admin_user_path(resource), {class: 'button'} %> 21 | 22 | -------------------------------------------------------------------------------- /app/views/attachments/_attachment.html.erb: -------------------------------------------------------------------------------- 1 | <% if attachment.image? %> 2 |
3 | 4 |
5 | <% else %> 6 | View Attachment 7 | <% end %> 8 | -------------------------------------------------------------------------------- /app/views/attachments/_inline_attachment.html.erb: -------------------------------------------------------------------------------- 1 | <% if attachment.image? %> 2 | 3 | <% else %> 4 | Files sent via text are attached to this email. 5 | <% end %> 6 | -------------------------------------------------------------------------------- /app/views/client_edit_markers/_client_edit_marker.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | <%= client_edit_marker.body %> 5 |

6 |
7 |

8 | <%= client_edit_marker.send_at.strftime('%-m/%-d/%y, %-l:%M%P') %> 9 |

10 |
11 | -------------------------------------------------------------------------------- /app/views/clients/_client_status.html.erb: -------------------------------------------------------------------------------- 1 | <% if current_user.department.client_statuses? && @relationships_by_status.any? %> 2 |
3 | <% @relationships_by_status.each do |status, ids| %> 4 |
5 |
6 |
7 | You have <%= ids.count %> <%= status.downcase %> <%= 'client'.pluralize(ids.count) %> due for follow up. 8 |
9 |
10 | <%= link_to "Message them", new_mass_message_path(reporting_relationships: ids), class: "button button--small with-no-padding" %> 11 |
12 |
13 |
14 | <% end %> 15 |
16 | <% end %> 17 | -------------------------------------------------------------------------------- /app/views/clients/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for(:template_name) { "clients" } %> 2 | 3 | <% content_for(:js) do %> 4 | <%= javascript_include_tag 'clients/edit' %> 5 | <% end %> 6 | 7 |
8 |
9 |
10 | <%= render 'clients/client_form', form_title: 'Edit client', save_text: 'Save changes' %> 11 | <%= render 'clients/client_manage', form_title: 'Edit client', save_text: 'Save changes' %> 12 |
13 |
14 |
15 | -------------------------------------------------------------------------------- /app/views/clients/index.js.erb: -------------------------------------------------------------------------------- 1 | $('#client-list').removeClass('hidden'); 2 | $('#client-list').replaceWith('<%= j render 'clients/clients' %>'); 3 | $('#client-empty-dialog').addClass('hidden'); 4 | clientListInit(); 5 | -------------------------------------------------------------------------------- /app/views/clients/new.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for(:js) do %> 2 | <%= javascript_include_tag 'clients/new' %> 3 | <% end %> 4 | 5 |
6 |
7 |
8 | <%= render 'client_form', form_title: 'Add a new client', save_text: 'Save new client' %> 9 |
10 |
11 |
12 | -------------------------------------------------------------------------------- /app/views/conversation_ends_markers/_conversation_ends_marker.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | <%= conversation_ends_marker.body %> 5 |

6 |
7 |

8 | <%= conversation_ends_marker.send_at.strftime('%-m/%-d/%y, %-l:%M%P') %> 9 |

10 |
11 | -------------------------------------------------------------------------------- /app/views/court_reminders/_court_reminder.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | <%= court_reminder.body %> 5 |
6 |
7 |

8 | <% if court_reminder.twilio_status == 'undelivered' %> 9 | <%= t('message.status.undelivered') %> 10 | <% else %> 11 | <%= court_reminder.twilio_status %> <%= court_reminder.send_at.to_s(:message_timestamp) %> 12 | <% end %> 13 |

14 |
15 | -------------------------------------------------------------------------------- /app/views/devise/confirmations/new.html.erb: -------------------------------------------------------------------------------- 1 |

Resend confirmation instructions

2 | 3 | <%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %> 4 | <%= devise_error_messages! %> 5 | 6 |
7 | <%= f.label :email %>
8 | <%= f.email_field :email, autofocus: true, value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email) %> 9 |
10 | 11 |
12 | <%= f.submit "Resend confirmation instructions" %> 13 |
14 | <% end %> 15 | 16 | <%= render "devise/shared/links" %> 17 | -------------------------------------------------------------------------------- /app/views/devise/mailer/confirmation_instructions.html.erb: -------------------------------------------------------------------------------- 1 |

Welcome <%= @email %>!

2 | 3 |

You can confirm your account email through the link below:

4 | 5 |

<%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %>

6 | -------------------------------------------------------------------------------- /app/views/devise/mailer/email_changed.html.erb: -------------------------------------------------------------------------------- 1 |

Hello <%= @email %>!

2 | 3 | <% if @resource.try(:unconfirmed_email?) %> 4 |

We're contacting you to notify you that your email is being changed to <%= @resource.unconfirmed_email %>.

5 | <% else %> 6 |

We're contacting you to notify you that your email has been changed to <%= @resource.email %>.

7 | <% end %> 8 | -------------------------------------------------------------------------------- /app/views/devise/mailer/password_change.html.erb: -------------------------------------------------------------------------------- 1 |

Hello <%= @resource.email %>!

2 | 3 |

We're contacting you to notify you that your password has been changed.

4 | -------------------------------------------------------------------------------- /app/views/devise/mailer/reset_password_instructions.html.erb: -------------------------------------------------------------------------------- 1 |

Hello <%= @resource.email %>!

2 | 3 |

Someone has requested a link to change your password. You can do this through the link below.

4 | 5 |

<%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %>

6 | 7 |

If you didn't request this, please ignore this email.

8 |

Your password won't change until you access the link above and create a new one.

9 | -------------------------------------------------------------------------------- /app/views/devise/mailer/unlock_instructions.html.erb: -------------------------------------------------------------------------------- 1 |

Hello <%= @resource.email %>!

2 | 3 |

Your account has been locked due to an excessive number of unsuccessful sign in attempts.

4 | 5 |

Click the link below to unlock your account:

6 | 7 |

<%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %>

8 | -------------------------------------------------------------------------------- /app/views/devise/passwords/new.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 |
7 |

Password reset

8 |

We can help. Simply enter your email address below, and we'll send you a link to reset your password.

9 |
10 | 11 | <%= form_for resource, as: resource_name, url: password_path(resource_name), builder: GcfFormBuilder do |f| %> 12 |
13 | <%= f.gcf_input_field :email, "Email", autofocus: true, classes: ['form-width--long'] %> 14 |
15 | 18 | <% end %> 19 | 20 |
21 |
22 |
23 |
24 | -------------------------------------------------------------------------------- /app/views/devise/shared/_links.html.erb: -------------------------------------------------------------------------------- 1 | <%- if controller_name != 'sessions' %> 2 | <%= link_to "Log in", new_session_path(resource_name) %>
3 | <% end -%> 4 | 5 | <%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> 6 | <%= link_to "Forgot your password?", new_password_path(resource_name) %>
7 | <% end -%> 8 | 9 | <%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> 10 | <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
11 | <% end -%> 12 | 13 | <%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> 14 | <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
15 | <% end -%> 16 | 17 | <%- if devise_mapping.omniauthable? %> 18 | <%- resource_class.omniauth_providers.each do |provider| %> 19 | <%= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider) %>
20 | <% end -%> 21 | <% end -%> 22 | -------------------------------------------------------------------------------- /app/views/devise/unlocks/new.html.erb: -------------------------------------------------------------------------------- 1 |

Resend unlock instructions

2 | 3 | <%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %> 4 | <%= devise_error_messages! %> 5 | 6 |
7 | <%= f.label :email %>
8 | <%= f.email_field :email, autofocus: true %> 9 |
10 | 11 |
12 | <%= f.submit "Resend unlock instructions" %> 13 |
14 | <% end %> 15 | 16 | <%= render "devise/shared/links" %> 17 | -------------------------------------------------------------------------------- /app/views/errors/internal_server_error.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |

There has been a problem on our end.

8 |

If you can, tell us what you were doing when this 9 | happened 10 | to help us fix the problem.

11 |

Otherwise, try starting from our homepage.

12 |
13 |
14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /app/views/errors/not_found.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |

Sorry, the page you are looking for doesn't exist.

8 |

If you followed a link to get here, please let us know. 9 |

10 |

Otherwise, try starting from our homepage.

11 |
12 |
13 |
14 |
15 |
16 | -------------------------------------------------------------------------------- /app/views/layouts/_flash.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |

<% if local_assigns[:link_to] %><%= body %><% else %><%= body %><% end %>

4 | 5 |
6 |
7 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= stylesheet_link_tag :email %> 5 | 6 | 7 | <%= yield %> 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /app/views/merged_with_markers/_merged_with_marker.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | <%= merged_with_marker.body %> 5 |

6 |
7 |

8 | <%= merged_with_marker.send_at.strftime('%-m/%-d/%y, %-l:%M%P') %> 9 |

10 |
11 | -------------------------------------------------------------------------------- /app/views/notification_mailer/message_notification.text.erb: -------------------------------------------------------------------------------- 1 | ClientComm 2 | 3 | <%= "#{@client.first_name} #{@client.last_name}" %> sent you a text message on <%= @message.created_at.strftime("%-m/%d") %>, at <%= @message.created_at.strftime("%-I:%M%p") %>: 4 | 5 | <% if @message.body.present? %> 6 | "<%= @message.body %>" 7 | <% end %> 8 | 9 | <%= '[Image attached, view on ClientComm]' if @message.attachments.present? %> 10 | <% rr = ReportingRelationship.find_by(client: @message.client, user: @message.user) %> 11 | Respond on ClientComm: <%= reporting_relationship_url(rr) %> 12 | -------------------------------------------------------------------------------- /app/views/reporting_relationships/_scheduled_messages_link.html.erb: -------------------------------------------------------------------------------- 1 | <%= link_to(reporting_relationship_scheduled_messages_index_path(rr), class: 'scheduled-messages-bar') do %> 2 | <%= pluralize(count, 'message') %> scheduled 3 | <% end %> 4 | -------------------------------------------------------------------------------- /app/views/scheduled_messages/index.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for(:template_name) { "messages" } %> 2 | 3 |
4 |
5 |
6 |

<%= @client.full_name %>

7 | <%= link_to "Manage client", edit_client_path(@client), {class: 'button button--small' } %> 8 |
9 | <%= phone_number_display(@client.phone_number) %> 10 |
11 |
12 | 13 |
14 |
15 | <%= render @messages %> 16 |
17 |
18 | 19 | <%= render "reporting_relationships/send_message_form", autofocus: false %> 20 | 21 | <%= render "scheduled_list_modal" %> 22 | -------------------------------------------------------------------------------- /app/views/text_messages/_text_message.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | <% text_message.body.each_line do |body_line| %> 5 | <%= body_line %>
6 | <% end %> 7 | <%= render text_message.attachments %> 8 |
9 |
10 | <%= render 'text_messages/text_message_status', text_message: text_message %> 11 |
12 | -------------------------------------------------------------------------------- /app/views/text_messages/_text_message_status.html.erb: -------------------------------------------------------------------------------- 1 |

2 | <% if text_message.twilio_status == 'undelivered' %> 3 | <%= t('message.status.undelivered') %> 4 | <% elsif text_message.twilio_status == 'blacklisted' %> 5 | <%= t('message.status.blacklisted') %> 6 | <% elsif [nil, 'queued', 'sent', 'sending'].include? text_message.twilio_status %> 7 | <%= t('message.status.sent') %> 8 | <% elsif text_message.twilio_status == 'maybe_undelivered' %> 9 | <%= t('message.status.maybe_undelivered') %> 10 | <% else %> 11 | <%= text_message.twilio_status %> <%= text_message.send_at.to_s(:message_timestamp) %> 12 | <% end %> 13 |

14 | -------------------------------------------------------------------------------- /app/views/transfer_markers/_transfer_marker.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | <%= transfer_marker.body %> 5 |

6 |
7 |

8 | <%= transfer_marker.send_at.strftime('%-m/%-d/%y, %-l:%M%P') %> 9 |

10 |
11 | -------------------------------------------------------------------------------- /bin/brakeman: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Script for running Brakeman tests 4 | # Brakeman is a security scanner https://github.com/presidentbeef/brakeman. 5 | 6 | set -e 7 | 8 | script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 9 | 10 | gem install brakeman -N 11 | brakeman --exit-on-warn . 12 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /bin/bundler-audit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Script for running bundler-audit tests 4 | # bundler-audit is a gem vulnerability scanner 5 | # https://github.com/rubysec/bundler-audit 6 | 7 | set -e 8 | 9 | script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 10 | 11 | gem install bundler-audit -N 12 | bundle-audit check --update --ignore CVE-2018-1000544 13 | -------------------------------------------------------------------------------- /bin/codeclimate: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 6 | 7 | bundle exec codeclimate-test-reporter coverage/.resultset.json 8 | -------------------------------------------------------------------------------- /bin/delayed_job: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'config', 'environment')) 4 | require 'delayed/command' 5 | Delayed::Command.new(ARGV).daemonize 6 | -------------------------------------------------------------------------------- /bin/pushit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 5 | 6 | git pull -r 7 | git rebase -i --exec 'git duet-commit --amend --reset-author' 8 | 9 | $script_dir/test 10 | 11 | git push 12 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../config/application', __dir__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('spring', __dir__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | 8 | require 'pathname' 9 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', 10 | Pathname.new(__FILE__).realpath) 11 | 12 | require 'rubygems' 13 | require 'bundler/setup' 14 | 15 | load Gem.bin_path('rspec-core', 'rspec') 16 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('..', __dir__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a starting point to setup your application. 15 | # Add necessary setup steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | # Install JavaScript dependencies if using Yarn 22 | # system('bin/yarn') 23 | 24 | # puts "\n== Copying sample files ==" 25 | # unless File.exist?('config/database.yml') 26 | # cp 'config/database.yml.sample', 'config/database.yml' 27 | # end 28 | 29 | puts "\n== Preparing database ==" 30 | system! 'bin/rails db:setup' 31 | 32 | puts "\n== Removing old logs and tempfiles ==" 33 | system! 'bin/rails log:clear tmp:clear' 34 | 35 | puts "\n== Restarting application server ==" 36 | system! 'bin/rails restart' 37 | end 38 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads spring without using Bundler, in order to be fast. 4 | # It gets overwritten when you run the `spring binstub` command. 5 | 6 | unless defined?(Spring) 7 | require 'rubygems' 8 | require 'bundler' 9 | 10 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) 11 | spring = lockfile.specs.detect { |spec| spec.name == 'spring' } 12 | if spring 13 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path 14 | gem 'spring', spring.version 15 | require 'spring/binstub' 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 6 | 7 | RAILS_ENV=test bundle exec rspec $@ 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('..', __dir__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a way to update your development environment automatically. 15 | # Add necessary update steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | puts "\n== Updating database ==" 22 | system! 'bin/rails db:migrate' 23 | 24 | puts "\n== Removing old logs and tempfiles ==" 25 | system! 'bin/rails log:clear tmp:clear' 26 | 27 | puts "\n== Restarting application server ==" 28 | system! 'bin/rails restart' 29 | end 30 | -------------------------------------------------------------------------------- /bin/yarn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | VENDOR_PATH = File.expand_path('..', __dir__) 3 | Dir.chdir(VENDOR_PATH) do 4 | begin 5 | exec "yarnpkg #{ARGV.join(" ")}" 6 | rescue Errno::ENOENT 7 | $stderr.puts 'Yarn executable was not detected in the system.' 8 | $stderr.puts 'Download Yarn at https://yarnpkg.com/en/docs/install' 9 | exit 1 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /co-authors: -------------------------------------------------------------------------------- 1 | Co-Authored-By: Mikela Clemmons 2 | Co-Authored-By: Zachary Auerbach 3 | Co-Authored-By: Tomas Apadoca 4 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative 'config/environment' 4 | require 'dotenv/load' if Rails.env.development? 5 | 6 | run Rails.application 7 | -------------------------------------------------------------------------------- /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' 5 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | production: &default 2 | adapter: postgresql 3 | 4 | development: 5 | <<: *default 6 | 7 | staging: 8 | <<: *default 9 | 10 | test: 11 | adapter: test 12 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ApplicationController.renderer.defaults.merge!( 4 | # http_host: 'example.org', 5 | # https: false 6 | # ) 7 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 11 | # Rails.application.config.assets.precompile += %w( search.js ) 12 | Rails.application.config.assets.precompile += %w[email.scss] 13 | Rails.application.config.assets.precompile += %w[clients/edit.js clients/new.js mass_messages.js messages.js reporting_relationships/welcomes.js] 14 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /config/initializers/cloudwatch.rb: -------------------------------------------------------------------------------- 1 | CLOUD_WATCH = if ENV['AWS_SECRET_ACCESS_KEY'] 2 | Aws::CloudWatch::Client.new( 3 | access_key_id: ENV['AWS_ACCESS_KEY_ID'], 4 | secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'], 5 | region: 'us-east-1' 6 | ) 7 | else 8 | Aws::CloudWatch::Client.new(stub_responses: true) 9 | end 10 | -------------------------------------------------------------------------------- /config/initializers/config.rb: -------------------------------------------------------------------------------- 1 | APP_CONFIG = YAML.load_file(Rails.root.join('config', 'config.yml'))[Rails.env] 2 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /config/initializers/fake_twilio.rb: -------------------------------------------------------------------------------- 1 | if Rails.env.test? 2 | require File.expand_path(Rails.root.join('spec', 'fake_twilio_client')) 3 | Twilio::REST.send(:remove_const, :Client) 4 | Twilio::REST::Client = FakeTwilioClient 5 | end 6 | -------------------------------------------------------------------------------- /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 += %i[ 5 | body 6 | password 7 | ] 8 | 9 | Rails.application.config.filter_parameters << lambda do |param_name, value| 10 | value.gsub!(/.+/, '[FILTERED]') if %w[From To].include?(param_name) && value.respond_to?(:gsub!) 11 | end 12 | -------------------------------------------------------------------------------- /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 | ActiveSupport::Inflector.inflections(:en) do |inflect| 18 | inflect.acronym 'CSV' 19 | end 20 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /config/initializers/mixpanel.rb: -------------------------------------------------------------------------------- 1 | mixpanel_token = ENV['MIXPANEL_TOKEN'] 2 | return if mixpanel_token.nil? 3 | 4 | MIXPANEL_TRACKER = Mixpanel::Tracker.new(mixpanel_token) 5 | # silence local SSL errors 6 | if Rails.env.development? 7 | Mixpanel.config_http do |http| 8 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /config/initializers/new_framework_defaults.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file contains migration options to ease your Rails 5.0 upgrade. 4 | # 5 | # Read the Rails 5.0 release notes for more info on each option. 6 | 7 | # Enable per-form CSRF tokens. Previous versions had false. 8 | Rails.application.config.action_controller.per_form_csrf_tokens = true 9 | 10 | # Enable origin-checking CSRF mitigation. Previous versions had false. 11 | Rails.application.config.action_controller.forgery_protection_origin_check = true 12 | 13 | # Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`. 14 | # Previous versions had false. 15 | ActiveSupport.to_time_preserves_timezone = true 16 | 17 | # Require `belongs_to` associations by default. Previous versions had false. 18 | Rails.application.config.active_record.belongs_to_required_by_default = true 19 | 20 | # Configure SSL options to enable HSTS with subdomains. Previous versions had false. 21 | Rails.application.config.ssl_options = { hsts: { subdomains: true } } 22 | -------------------------------------------------------------------------------- /config/initializers/new_framework_defaults_5_1.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file contains migration options to ease your Rails 5.1 upgrade. 4 | # 5 | # Once upgraded flip defaults one by one to migrate to the new default. 6 | # 7 | # Read the Guide for Upgrading Ruby on Rails for more info on each option. 8 | 9 | # Make `form_with` generate non-remote forms. 10 | Rails.application.config.action_view.form_with_generates_remote_forms = false 11 | 12 | # Unknown asset fallback will return the path passed in when the given 13 | # asset is not present in the asset pipeline. 14 | # Rails.application.config.assets.unknown_asset_fallback = false 15 | -------------------------------------------------------------------------------- /config/initializers/paperclip.rb: -------------------------------------------------------------------------------- 1 | Paperclip::UriAdapter.register 2 | 3 | Paperclip.options[:content_type_mappings] = { 4 | amr: 'application/octet-stream', 5 | '3gp': 'application/octet-stream' 6 | } 7 | -------------------------------------------------------------------------------- /config/initializers/premailer_rails.rb: -------------------------------------------------------------------------------- 1 | Premailer::Rails.config[:line_length] = 180 2 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_clientcomm_session' 4 | -------------------------------------------------------------------------------- /config/initializers/time_formats.rb: -------------------------------------------------------------------------------- 1 | Time::DATE_FORMATS[:message_timestamp] = '%-m/%-d/%y at %I:%M%P' 2 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | %w( 2 | .ruby-version 3 | .rbenv-vars 4 | tmp/restart.txt 5 | tmp/caching-dev.txt 6 | ).each { |path| Spring.watch(path) } 7 | -------------------------------------------------------------------------------- /db/migrate/20170511080820_create_clients.rb: -------------------------------------------------------------------------------- 1 | class CreateClients < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :clients do |t| 4 | t.string :first_name 5 | t.string :last_name 6 | t.datetime :birth_date 7 | t.string :phone_number 8 | t.boolean :active, default: true, null: false 9 | 10 | t.timestamps null: false 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20170517213000_add_user_ref_to_clients.rb: -------------------------------------------------------------------------------- 1 | class AddUserRefToClients < ActiveRecord::Migration[5.0] 2 | def change 3 | add_reference :clients, :user, foreign_key: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170519034001_add_full_name_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddFullNameToUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :users, :full_name, :string, default: "" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170519215508_create_messages.rb: -------------------------------------------------------------------------------- 1 | class CreateMessages < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :messages do |t| 4 | t.references :client, foreign_key: true 5 | t.references :user, foreign_key: true 6 | t.string :body, default: "" 7 | t.string :number_from, null: false 8 | t.string :number_to, null: false 9 | t.boolean :inbound, default: false, null: false 10 | t.string :twilio_sid 11 | t.string :twilio_status 12 | 13 | t.timestamps 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /db/migrate/20170523081015_add_index_to_messages.rb: -------------------------------------------------------------------------------- 1 | class AddIndexToMessages < ActiveRecord::Migration[5.0] 2 | def change 3 | add_index :messages, :twilio_sid 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170608050028_add_read_to_messages.rb: -------------------------------------------------------------------------------- 1 | class AddReadToMessages < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :messages, :read, :boolean, default: false 4 | 5 | # read defaults to false, but all messages that exist when the migration 6 | # happens should have read = true 7 | Message.update(read: true) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20170619175734_create_attachments.rb: -------------------------------------------------------------------------------- 1 | class CreateAttachments < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :attachments do |t| 4 | t.string :url, null: false 5 | t.string :content_type 6 | t.references :message, foreign_key: true, null: false 7 | t.integer :height 8 | t.integer :width 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20170706230335_add_send_date_to_messages.rb: -------------------------------------------------------------------------------- 1 | class AddSendDateToMessages < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :messages, :send_date, :datetime 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170711222129_devise_invitable_add_to_users.rb: -------------------------------------------------------------------------------- 1 | class DeviseInvitableAddToUsers < ActiveRecord::Migration[5.0] 2 | def up 3 | change_table :users do |t| 4 | t.string :invitation_token 5 | t.datetime :invitation_created_at 6 | t.datetime :invitation_sent_at 7 | t.datetime :invitation_accepted_at 8 | t.integer :invitation_limit 9 | t.references :invited_by, polymorphic: true 10 | t.integer :invitations_count, default: 0 11 | t.index :invitations_count 12 | t.index :invitation_token, unique: true # for invitable 13 | t.index :invited_by_id 14 | end 15 | end 16 | 17 | def down 18 | change_table :users do |t| 19 | t.remove_references :invited_by, polymorphic: true 20 | t.remove :invitations_count, :invitation_limit, :invitation_sent_at, :invitation_accepted_at, :invitation_token, :invitation_created_at 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /db/migrate/20170714175419_rename_send_date_to_send_at_in_messages.rb: -------------------------------------------------------------------------------- 1 | class RenameSendDateToSendAtInMessages < ActiveRecord::Migration[5.0] 2 | def change 3 | rename_column :messages, :send_date, :send_at 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170718001857_remove_birth_date_from_clients.rb: -------------------------------------------------------------------------------- 1 | class RemoveBirthDateFromClients < ActiveRecord::Migration[5.0] 2 | def change 3 | remove_column :clients, :birth_date, :datetime 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170728173551_add_email_subscribe_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddEmailSubscribeToUser < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :users, :email_subscribe, :boolean, default: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170731233514_add_archived_to_client.rb: -------------------------------------------------------------------------------- 1 | class AddArchivedToClient < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :clients, :archived, :boolean, default: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170801221946_add_lock_version_to_messages.rb: -------------------------------------------------------------------------------- 1 | class AddLockVersionToMessages < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :messages, :lock_version, :integer, default: 0 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170801222108_add_sent_to_messages.rb: -------------------------------------------------------------------------------- 1 | class AddSentToMessages < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :messages, :sent, :boolean, default: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170803184914_set_send_at_on_messages.rb: -------------------------------------------------------------------------------- 1 | class SetSendAtOnMessages < ActiveRecord::Migration[5.0] 2 | def change 3 | Message.all.each do |message| 4 | message.update(send_at: message.created_at) if message.send_at.nil? 5 | end 6 | 7 | change_column :messages, :send_at, :datetime, null: false 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20170804173345_remove_archived_from_client.rb: -------------------------------------------------------------------------------- 1 | class RemoveArchivedFromClient < ActiveRecord::Migration[5.0] 2 | def change 3 | remove_column :clients, :archived, :boolean 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170815171039_create_active_admin_comments.rb: -------------------------------------------------------------------------------- 1 | class CreateActiveAdminComments < ActiveRecord::Migration::Current 2 | def self.up 3 | create_table :active_admin_comments do |t| 4 | t.string :namespace 5 | t.text :body 6 | t.references :resource, polymorphic: true 7 | t.references :author, polymorphic: true 8 | t.timestamps 9 | end 10 | add_index :active_admin_comments, [:namespace] 11 | end 12 | 13 | def self.down 14 | drop_table :active_admin_comments 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /db/migrate/20170823233745_add_notes_to_clients.rb: -------------------------------------------------------------------------------- 1 | class AddNotesToClients < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :clients, :notes, :text 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170901230857_add_last_contacted_at_to_clients.rb: -------------------------------------------------------------------------------- 1 | class AddLastContactedAtToClients < ActiveRecord::Migration[5.0] 2 | def up 3 | add_column :clients, :last_contacted_at, :datetime, null: false, default: -> { 'NOW()' } 4 | add_column :clients, :has_unread_messages, :boolean, null: false, default: false 5 | end 6 | 7 | def down 8 | remove_column :clients, :last_contacted_at 9 | remove_column :clients, :has_unread_messages 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20170911173225_add_index_to_clients.rb: -------------------------------------------------------------------------------- 1 | class AddIndexToClients < ActiveRecord::Migration[5.0] 2 | def change 3 | add_index :clients, :phone_number, unique: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170911215234_add_active_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddActiveToUser < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :users, :active, :boolean, null: false, default: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170913221609_add_has_message_error_to_clients.rb: -------------------------------------------------------------------------------- 1 | class AddHasMessageErrorToClients < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :clients, :has_message_error, :boolean, default: false, null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170920220501_make_last_contacted_at_nullable.rb: -------------------------------------------------------------------------------- 1 | class MakeLastContactedAtNullable < ActiveRecord::Migration[5.1] 2 | def up 3 | change_column :clients, :last_contacted_at, :datetime, null: true, default: nil 4 | end 5 | 6 | def down 7 | change_column :clients, :last_contacted_at, :datetime, null: false, default: -> { 'NOW()' } 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20170926210630_create_templates.rb: -------------------------------------------------------------------------------- 1 | class CreateTemplates < ActiveRecord::Migration[5.1] 2 | def up 3 | create_table :templates do |t| 4 | t.string :title 5 | t.text :body 6 | end 7 | end 8 | 9 | def down 10 | drop_table :templates 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20170926211409_add_templates_ref_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddTemplatesRefToUsers < ActiveRecord::Migration[5.1] 2 | def change 3 | add_reference :templates, :user, foreign_key: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170929175554_add_feature_flags_table.rb: -------------------------------------------------------------------------------- 1 | class AddFeatureFlagsTable < ActiveRecord::Migration[5.1] 2 | def up 3 | create_table :feature_flags do |t| 4 | t.string :flag 5 | t.boolean :enabled, null: false 6 | end 7 | 8 | FeatureFlag.create!( 9 | flag: "mass_messages", 10 | enabled: (ENV["MASS_MESSAGES"] == 'true') 11 | ) 12 | 13 | FeatureFlag.create!( 14 | flag: "allow_signups", 15 | enabled: (ENV["ALLOW_SIGNUPS"] == 'true') 16 | ) 17 | 18 | FeatureFlag.create!( 19 | flag: "templates", 20 | enabled: false 21 | ) 22 | end 23 | 24 | def down 25 | drop_table :feature_flags 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /db/migrate/20171003212709_rename_attachment_to_legacy_attachment.rb: -------------------------------------------------------------------------------- 1 | class RenameAttachmentToLegacyAttachment < ActiveRecord::Migration[5.1] 2 | def change 3 | rename_table :attachments, :legacy_attachments 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20171003215315_create_paperclip_attachments.rb: -------------------------------------------------------------------------------- 1 | class CreatePaperclipAttachments < ActiveRecord::Migration[5.1] 2 | def self.up 3 | create_table :attachments do |t| 4 | t.references :message, foreign_key: true, null: false 5 | t.attachment :media 6 | end 7 | end 8 | 9 | def self.down 10 | drop_table :attachments 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20171004220005_migrate_old_attachments.rb: -------------------------------------------------------------------------------- 1 | class MigrateOldAttachments < ActiveRecord::Migration[5.1] 2 | def up 3 | LegacyAttachment.all.each do |old_attachment| 4 | new_attachment = Attachment.new 5 | new_attachment.media_remote_url = old_attachment.url 6 | new_attachment.message_id = old_attachment.message_id 7 | new_attachment.save! 8 | end 9 | 10 | drop_table :legacy_attachments 11 | end 12 | 13 | class LegacyAttachment < ApplicationRecord; end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/20171009214412_remove_devise_invitable.rb: -------------------------------------------------------------------------------- 1 | class RemoveDeviseInvitable < ActiveRecord::Migration[5.1] 2 | def up 3 | change_table :users do |t| 4 | t.remove_references :invited_by, polymorphic: true 5 | t.remove :invitations_count, :invitation_limit, :invitation_sent_at, :invitation_accepted_at, :invitation_token, :invitation_created_at 6 | end 7 | end 8 | 9 | def down 10 | change_table :users do |t| 11 | t.string :invitation_token 12 | t.datetime :invitation_created_at 13 | t.datetime :invitation_sent_at 14 | t.datetime :invitation_accepted_at 15 | t.integer :invitation_limit 16 | t.references :invited_by, polymorphic: true 17 | t.integer :invitations_count, default: 0 18 | t.index :invitations_count 19 | t.index :invitation_token, unique: true # for invitable 20 | t.index :invited_by_id 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /db/migrate/20171009234615_remove_signups.rb: -------------------------------------------------------------------------------- 1 | class RemoveSignups < ActiveRecord::Migration[5.1] 2 | def change 3 | flag = FeatureFlag.find_by(flag: 'allow_signups') 4 | flag.destroy if flag.present? 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20171009235722_make_full_name_non_nullable.rb: -------------------------------------------------------------------------------- 1 | class MakeFullNameNonNullable < ActiveRecord::Migration[5.1] 2 | def up 3 | User.where(full_name: nil).each do |user| 4 | user.update!(full_name: user.email) 5 | end 6 | 7 | change_column :users, :full_name, :string, null: false, default: nil 8 | end 9 | 10 | def down 11 | change_column :users, :full_name, :string, null: true, default: '' 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20171010185346_add_desk_phone_number_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddDeskPhoneNumberToUser < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :users, :desk_phone_number, :string, null: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20171016161803_rename_user_desk_phone_number_to_phone_number.rb: -------------------------------------------------------------------------------- 1 | class RenameUserDeskPhoneNumberToPhoneNumber < ActiveRecord::Migration[5.1] 2 | def change 3 | rename_column :users, :desk_phone_number, :phone_number 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20171016173705_add_unclaimed_user_to_unclaimed_clients.rb: -------------------------------------------------------------------------------- 1 | class AddUnclaimedUserToUnclaimedClients < ActiveRecord::Migration[5.1] 2 | def change 3 | Client.where(user_id: nil).each do |client| 4 | User.find_by(email: ENV['UNCLAIMED_EMAIL']).clients << client 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20171017184404_add_client_statuses_table.rb: -------------------------------------------------------------------------------- 1 | class AddClientStatusesTable < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :client_statuses do |t| 4 | t.string :name, null: false 5 | end 6 | 7 | add_reference :clients, :client_status, foreign_key: true 8 | end 9 | end 10 | 11 | class ClientStatus < ApplicationRecord 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20171020012824_add_follup_up_date_to_client_statuses.rb: -------------------------------------------------------------------------------- 1 | class AddFollupUpDateToClientStatuses < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :client_statuses, :followup_date, :integer 4 | ClientStatus.reset_column_information 5 | 6 | active_status = ClientStatus.find_by(name: 'Active') 7 | active_status.update!(followup_date: 25) if active_status 8 | 9 | training_status = ClientStatus.find_by(name: 'Training') 10 | training_status.update!(followup_date: 25) if training_status 11 | 12 | exited_status = ClientStatus.find_by(name: 'Exited') 13 | exited_status.update!(followup_date: 85) if exited_status 14 | 15 | change_column_null :client_statuses, :followup_date, false 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /db/migrate/20171021000847_rename_email_subscribe_to_message_notification_emails.rb: -------------------------------------------------------------------------------- 1 | class RenameEmailSubscribeToMessageNotificationEmails < ActiveRecord::Migration[5.1] 2 | def change 3 | rename_column :users, :email_subscribe, :message_notification_emails 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20171030170455_create_departments.rb: -------------------------------------------------------------------------------- 1 | class CreateDepartments < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :departments do |t| 4 | t.string :name 5 | t.string :phone_number 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20171030170543_add_departments_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddDepartmentsToUsers < ActiveRecord::Migration[5.1] 2 | def change 3 | add_reference :users, :department, foreign_key: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20171031180320_create_reporting_relationships.rb: -------------------------------------------------------------------------------- 1 | class CreateReportingRelationships < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :reporting_relationships do |t| 4 | t.references :user, foreign_key: true 5 | t.references :client, foreign_key: true 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20171101221037_add_active_to_reporting_relationship.rb: -------------------------------------------------------------------------------- 1 | class AddActiveToReportingRelationship < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :reporting_relationships, :active, :boolean, default: true, null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20171107212003_move_active_from_clients_to_reporting_relationships.rb: -------------------------------------------------------------------------------- 1 | class MoveActiveFromClientsToReportingRelationships < ActiveRecord::Migration[5.1] 2 | def up 3 | Client.all.find_each do |client| 4 | ReportingRelationship.find_or_create_by( 5 | user_id: client['user_id'], 6 | client: client 7 | ) do |rr| 8 | rr.active = client[:active] 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20171109222018_add_user_to_department.rb: -------------------------------------------------------------------------------- 1 | class AddUserToDepartment < ActiveRecord::Migration[5.1] 2 | def change 3 | add_reference :departments, :user, foreign_key: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20171129184319_add_metadata_values_to_reporting_relationship.rb: -------------------------------------------------------------------------------- 1 | class AddMetadataValuesToReportingRelationship < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :reporting_relationships, :notes, :text 4 | add_column :reporting_relationships, :last_contacted_at, :datetime 5 | add_column :reporting_relationships, :has_unread_messages, :boolean, default: false, null: false 6 | add_column :reporting_relationships, :has_message_error, :boolean, default: false, null: false 7 | add_reference :reporting_relationships, :client_status, foreign_key: true 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20180105005217_create_survey_questions.rb: -------------------------------------------------------------------------------- 1 | class CreateSurveyQuestions < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :survey_questions do |t| 4 | t.text :text 5 | 6 | t.timestamps 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20180105005654_create_survey_responses.rb: -------------------------------------------------------------------------------- 1 | class CreateSurveyResponses < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :survey_responses do |t| 4 | t.text :text 5 | t.references :survey_question, foreign_key: true 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20180105010123_create_surveys.rb: -------------------------------------------------------------------------------- 1 | class CreateSurveys < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :surveys do |t| 4 | t.references :client, foreign_key: true 5 | t.references :user, foreign_key: true 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20180105182759_create_survey_response_links.rb: -------------------------------------------------------------------------------- 1 | class CreateSurveyResponseLinks < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :survey_response_links do |t| 4 | t.references :survey, foreign_key: true 5 | t.references :survey_response, foreign_key: true 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20180112232629_remove_columns_from_client.rb: -------------------------------------------------------------------------------- 1 | class RemoveColumnsFromClient < ActiveRecord::Migration[5.1] 2 | def change 3 | remove_column :clients, :notes, :text 4 | remove_reference :clients, :client_status, foreign_key: true 5 | remove_column :clients, :active, :boolean 6 | remove_column :clients, :has_message_error, :boolean 7 | remove_column :clients, :has_unread_messages, :boolean 8 | remove_column :clients, :last_contacted_at, :datetime 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20180201222656_add_last_twilio_update_to_message.rb: -------------------------------------------------------------------------------- 1 | class AddLastTwilioUpdateToMessage < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :messages, :last_twilio_update, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180206221711_add_transfer_marker_to_messages.rb: -------------------------------------------------------------------------------- 1 | class AddTransferMarkerToMessages < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :messages, :transfer_marker, :bool, default: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180209191924_create_reports.rb: -------------------------------------------------------------------------------- 1 | class CreateReports < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :reports do |t| 4 | t.string :email, null: false 5 | t.references :department, foreign_key: true, null: false 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20180220183545_add_icon_color_to_client_status.rb: -------------------------------------------------------------------------------- 1 | class AddIconColorToClientStatus < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :client_statuses, :icon_color, :string, limit: 7 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180220194359_add_department_to_client_status.rb: -------------------------------------------------------------------------------- 1 | class AddDepartmentToClientStatus < ActiveRecord::Migration[5.1] 2 | def change 3 | add_reference :client_statuses, :department, key: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180222191401_add_in_treatment_group_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddInTreatmentGroupToUsers < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :users, :in_treatment_group, :boolean, default: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180222202720_remove_in_treatment_group_from_users.rb: -------------------------------------------------------------------------------- 1 | class RemoveInTreatmentGroupFromUsers < ActiveRecord::Migration[5.1] 2 | def change 3 | remove_column :users, :in_treatment_group, :boolean 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180228011744_make_followup_date_nullable_on_client_statuses.rb: -------------------------------------------------------------------------------- 1 | class MakeFollowupDateNullableOnClientStatuses < ActiveRecord::Migration[5.1] 2 | def change 3 | change_column_null :client_statuses, :followup_date, true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180305231922_add_autoreply_to_department.rb: -------------------------------------------------------------------------------- 1 | class AddAutoreplyToDepartment < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :departments, :unclaimed_response, :text 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180307215710_add_treatment_group_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddTreatmentGroupToUser < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :users, :treatment_group, :text 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180311020035_change_user_treatment_group_to_string.rb: -------------------------------------------------------------------------------- 1 | class ChangeUserTreatmentGroupToString < ActiveRecord::Migration[5.1] 2 | def change 3 | change_column :users, :treatment_group, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180411193449_add_node_id_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddNodeIdToUser < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :users, :node_id, :bigint 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180413174714_add_node_ids_to_client.rb: -------------------------------------------------------------------------------- 1 | class AddNodeIdsToClient < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :clients, :node_client_id, :bigint 4 | add_column :clients, :node_comm_id, :bigint 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20180419203735_add_category_to_reporting_relationship.rb: -------------------------------------------------------------------------------- 1 | class AddCategoryToReportingRelationship < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :reporting_relationships, :category, :string, default: 'no_cat' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180426170351_add_category_feature_flag.rb: -------------------------------------------------------------------------------- 1 | class AddCategoryFeatureFlag < ActiveRecord::Migration[5.1] 2 | def up 3 | FeatureFlag.create!( 4 | flag: 'categories', 5 | enabled: false 6 | ) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20180507185215_add_like_message_to_message.rb: -------------------------------------------------------------------------------- 1 | class AddLikeMessageToMessage < ActiveRecord::Migration[5.1] 2 | def change 3 | add_reference :messages, :like_message, foreign_key: { to_table: :messages } 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180516173227_add_id_number_to_clients.rb: -------------------------------------------------------------------------------- 1 | class AddIdNumberToClients < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :clients, :id_number, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180516175755_create_client_id_feature_flag.rb: -------------------------------------------------------------------------------- 1 | class CreateClientIdFeatureFlag < ActiveRecord::Migration[5.1] 2 | def up 3 | FeatureFlag.find_or_create_by(flag: 'client_id_number').update!(enabled: false) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180516183029_create_hide_notes_feature_flag.rb: -------------------------------------------------------------------------------- 1 | class CreateHideNotesFeatureFlag < ActiveRecord::Migration[5.1] 2 | def up 3 | FeatureFlag.find_or_create_by(flag: 'hide_notes').update!(enabled: false) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180518203229_create_court_date_csv.rb: -------------------------------------------------------------------------------- 1 | class CreateCourtDateCSV < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :court_date_csvs do |t| 4 | t.attachment :file 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20180605211155_add_admin_user_to_court_date_csv.rb: -------------------------------------------------------------------------------- 1 | class AddAdminUserToCourtDateCSV < ActiveRecord::Migration[5.1] 2 | def change 3 | add_reference :court_date_csvs, :admin_user, foreign_key: { to_table: :admin_users }, null: false, default: 1 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180606182912_add_court_date_csv_to_message.rb: -------------------------------------------------------------------------------- 1 | class AddCourtDateCSVToMessage < ActiveRecord::Migration[5.1] 2 | def change 3 | add_reference :messages, :court_date_csv, foreign_key: { to_table: :court_date_csvs } 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180608172907_make_to_and_from_nullable_on_message.rb: -------------------------------------------------------------------------------- 1 | class MakeToAndFromNullableOnMessage < ActiveRecord::Migration[5.1] 2 | def up 3 | change_column :messages, :number_to, :string, null: true 4 | change_column :messages, :number_from, :string, null: true 5 | end 6 | 7 | def down 8 | change_column :messages, :number_to, :string, null: false, default: '+15555555555' 9 | change_column :messages, :number_from, :string, null: false, default: '+15555555555' 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20180613212947_add_court_date_to_client.rb: -------------------------------------------------------------------------------- 1 | class AddCourtDateToClient < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :clients, :next_court_date_at, :date, null: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180618172611_add_court_dates_feature_flag.rb: -------------------------------------------------------------------------------- 1 | class AddCourtDatesFeatureFlag < ActiveRecord::Migration[5.1] 2 | def up 3 | FeatureFlag.create!( 4 | flag: 'court_dates', 5 | enabled: false 6 | ) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20180618234037_add_scheduled_message_count_feature_flag.rb: -------------------------------------------------------------------------------- 1 | class AddScheduledMessageCountFeatureFlag < ActiveRecord::Migration[5.1] 2 | def up 3 | FeatureFlag.create!( 4 | flag: 'scheduled_message_count', 5 | enabled: false 6 | ) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20180620203710_add_next_court_date_set_by_user_to_client.rb: -------------------------------------------------------------------------------- 1 | class AddNextCourtDateSetByUserToClient < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :clients, :next_court_date_set_by_user, :boolean, default: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180626210517_add_user_client_index_to_reporting_relationship.rb: -------------------------------------------------------------------------------- 1 | class AddUserClientIndexToReportingRelationship < ActiveRecord::Migration[5.1] 2 | def change 3 | add_index :reporting_relationships, %i[client_id user_id] 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180627173300_remove_locking_from_messages.rb: -------------------------------------------------------------------------------- 1 | class RemoveLockingFromMessages < ActiveRecord::Migration[5.1] 2 | def change 3 | remove_column :messages, :lock_version, :integer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180627210514_mark_inactive_messages_as_read.rb: -------------------------------------------------------------------------------- 1 | class Message < ApplicationRecord 2 | belongs_to :reporting_relationship, class_name: 'ReportingRelationship', foreign_key: 'reporting_relationship_id' 3 | end 4 | 5 | class ReportingRelationship < ApplicationRecord 6 | has_many :messages, dependent: :nullify 7 | end 8 | 9 | class MarkInactiveMessagesAsRead < ActiveRecord::Migration[5.1] 10 | def change 11 | msgs = Message.joins(:reporting_relationship).where(read: false, reporting_relationships: { active: false }) 12 | msgs.update(read: true) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/20180628172800_add_dimensions_to_attachments.rb: -------------------------------------------------------------------------------- 1 | class Attachment < ApplicationRecord 2 | belongs_to :message 3 | 4 | has_attached_file :media 5 | validates_attachment_content_type :media, content_type: %r{video\/.*|audio\/.*|image\/.*|text\/.*|application\/pdf.*} 6 | serialize :dimensions, Array 7 | 8 | def image? 9 | media_content_type.match?(%r{image\/.*}) ? true : false 10 | end 11 | end 12 | 13 | class AddDimensionsToAttachments < ActiveRecord::Migration[5.1] 14 | def up 15 | add_column :attachments, :dimensions, :string 16 | Attachment.reset_column_information 17 | Attachment.find_each(batch_size: 50).with_index do |att, i| 18 | next unless att.image? 19 | path = "/tmp/#{i}" 20 | att.media.copy_to_local_file(:original, path) 21 | f = open(path) 22 | geometry = Paperclip::Geometry.from_file(f) 23 | att.dimensions = [geometry.width.to_i, geometry.height.to_i] 24 | att.save! 25 | end 26 | end 27 | 28 | def down 29 | remove_column :attachments, :dimensions, :string 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /db/migrate/20180703172919_create_change_image.rb: -------------------------------------------------------------------------------- 1 | class CreateChangeImage < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :change_images do |t| 4 | t.attachment :file 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20180703180216_add_admin_user_to_change_image.rb: -------------------------------------------------------------------------------- 1 | class AddAdminUserToChangeImage < ActiveRecord::Migration[5.1] 2 | def change 3 | add_reference :change_images, :admin_user, foreign_key: { to_table: :admin_users }, null: false, default: 1 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180719184142_add_has_unread_messages_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddHasUnreadMessagesToUser < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :users, :has_unread_messages, :boolean, default: false, null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180724180654_add_cron_to_delayed_jobs.rb: -------------------------------------------------------------------------------- 1 | class AddCronToDelayedJobs < ActiveRecord::Migration[5.1] 2 | def self.up 3 | add_column :delayed_jobs, :cron, :string 4 | end 5 | 6 | def self.down 7 | remove_column :delayed_jobs, :cron 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20180730232332_add_index_to_messages_send_at.rb: -------------------------------------------------------------------------------- 1 | class AddIndexToMessagesSendAt < ActiveRecord::Migration[5.1] 2 | def change 3 | add_index :messages, :send_at 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180731175831_drop_active_admin_comments_table.rb: -------------------------------------------------------------------------------- 1 | class DropActiveAdminCommentsTable < ActiveRecord::Migration[5.1] 2 | def self.up 3 | drop_table :active_admin_comments 4 | end 5 | 6 | def self.down 7 | create_table :active_admin_comments do |t| 8 | t.string :namespace 9 | t.text :body 10 | t.references :resource, polymorphic: true 11 | t.references :author, polymorphic: true 12 | t.timestamps 13 | end 14 | add_index :active_admin_comments, [:namespace] 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /db/migrate/20180731212736_add_admin_flag_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddAdminFlagToUser < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :users, :admin, :boolean, default: false 4 | 5 | User.where('email ILIKE ?', '%codeforamerica.org').each do |user| 6 | user.update!(admin: true) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20180806183847_create_highlight_blobs.rb: -------------------------------------------------------------------------------- 1 | class CreateHighlightBlobs < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :highlight_blobs do |t| 4 | t.text :text 5 | 6 | t.timestamps 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20180813181128_drop_templates.rb: -------------------------------------------------------------------------------- 1 | class DropTemplates < ActiveRecord::Migration[5.1] 2 | def up 3 | drop_table :templates 4 | end 5 | 6 | def down 7 | create_table :templates do |t| 8 | t.string :title 9 | t.text :body 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20180816181102_remove_scheduled_message_jobs.rb: -------------------------------------------------------------------------------- 1 | class RemoveScheduledMessageJobs < ActiveRecord::Migration[5.1] 2 | def up 3 | Delayed::Job.where('handler ilike ?', '%ScheduledMessageJob%').destroy_all 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180816222016_cleanup_message_history.rb: -------------------------------------------------------------------------------- 1 | class CleanupMessageHistory < ActiveRecord::Migration[5.1] 2 | def up 3 | # rubocop:disable Rails/SkipsModelValidations 4 | past_messages = TextMessage.where(sent: false).where(inbound: false).where('send_at < ?', Time.zone.now - 1.hour) 5 | past_messages.where.not(twilio_sid: nil).update_all(sent: true) 6 | past_messages.where(twilio_sid: nil).update_all(twilio_status: 'undelivered', sent: true) 7 | # rubocop:enable Rails/SkipsModelValidations 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20180921232040_add_active_to_survey_responses.rb: -------------------------------------------------------------------------------- 1 | class AddActiveToSurveyResponses < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :survey_responses, :active, :boolean, null: false, default: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /deploy/playbooks/inventories/cc-gwen/group_vars/all/vars: -------------------------------------------------------------------------------- 1 | primary_user: 2 | gwen 3 | users: 4 | - { name: gwen, comment: "Gwen Rino primary user", state: "present" } 5 | - { name: tomas, comment: "Tomas", state: "present" } 6 | - { name: zak, comment: "Zak", state: "present" } 7 | - { name: andrew, comment: "Andrew", state: "present" } 8 | - { name: mikela, comment: "Mikela Clemmons", state: "absent" } 9 | -------------------------------------------------------------------------------- /deploy/playbooks/inventories/cc-gwen/hosts: -------------------------------------------------------------------------------- 1 | [data] 2 | ec2-54-224-241-222.compute-1.amazonaws.com 3 | 4 | -------------------------------------------------------------------------------- /deploy/playbooks/roles/users/defaults/main.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/deploy/playbooks/roles/users/defaults/main.yml -------------------------------------------------------------------------------- /deploy/playbooks/roles/users/pubkeys/andrew.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDQ3ckfbrWuJUxqDxdz4/+2aG7Wdq6dRLGk9vfx1GBTcUUf0k+/mRFIGfHHfHcQJtz1ByUN5LMcyUHxTEVcrab8OK8e/mRS3IZezFFyuMtCpW515m6y5rvyJ/VHTJxYPqPUWOLeBq8kGHnwt2dt8EU/ARvCAbj1AEto2jTpgOwURRSwEWJDmPsI6OvwU4k8MIe1a9Y3b20gLaWEBvf9IB+UEnULahQ4B4BfyHwhy9gY8ip49Ukr8gfqizCd4n7v3WmdJgu2Zs8o89FO8iwS0EtQMh9/V+otBtbwGdztpZH0/cAXf5kQWUkm+autS1cez7fKSVkuBkkMtMVhPVvUwoWWYYRCqhFAJwoZrAYK75DkD1V2PGucldVpwIYd9JsxIZJjq14X0v+sHbW/07okxA0gbFoe93j5VkN3rnJnmTUUq68FHbju/60LiwTR7fO0Ct2LXNX6ryJxFHjS0ucyqjgJlE6ZNN6X2lot2wai7S+T4k8g9raXMVeh4oIDJFgPu8LkGEuWdZFh9Fyqmmtvg8iOUH+8UZlcRqRLF+vDHfC3Qd7G856gNd0iAG+Q83fH7Yogux0beDmnSZ+EyvrG3N05PswKSxJeYW4uU5qgV5/tKDXJ1aQB85B0Fe1BCGvp+zQ/fi1n9Llpo0iENjymn0S56zJqjVrudGadcomNs/duUw== hyd415@gmail.com 2 | -------------------------------------------------------------------------------- /deploy/playbooks/roles/users/pubkeys/andrewhyder.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDQ3ckfbrWuJUxqDxdz4/+2aG7Wdq6dRLGk9vfx1GBTcUUf0k+/mRFIGfHHfHcQJtz1ByUN5LMcyUHxTEVcrab8OK8e/mRS3IZezFFyuMtCpW515m6y5rvyJ/VHTJxYPqPUWOLeBq8kGHnwt2dt8EU/ARvCAbj1AEto2jTpgOwURRSwEWJDmPsI6OvwU4k8MIe1a9Y3b20gLaWEBvf9IB+UEnULahQ4B4BfyHwhy9gY8ip49Ukr8gfqizCd4n7v3WmdJgu2Zs8o89FO8iwS0EtQMh9/V+otBtbwGdztpZH0/cAXf5kQWUkm+autS1cez7fKSVkuBkkMtMVhPVvUwoWWYYRCqhFAJwoZrAYK75DkD1V2PGucldVpwIYd9JsxIZJjq14X0v+sHbW/07okxA0gbFoe93j5VkN3rnJnmTUUq68FHbju/60LiwTR7fO0Ct2LXNX6ryJxFHjS0ucyqjgJlE6ZNN6X2lot2wai7S+T4k8g9raXMVeh4oIDJFgPu8LkGEuWdZFh9Fyqmmtvg8iOUH+8UZlcRqRLF+vDHfC3Qd7G856gNd0iAG+Q83fH7Yogux0beDmnSZ+EyvrG3N05PswKSxJeYW4uU5qgV5/tKDXJ1aQB85B0Fe1BCGvp+zQ/fi1n9Llpo0iENjymn0S56zJqjVrudGadcomNs/duUw== hyd415@gmail.com 2 | -------------------------------------------------------------------------------- /deploy/playbooks/roles/users/pubkeys/anule.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDWNSi25/rzu7PQTH4MVeCdqAQE3Jadu4yvReyU+jxtVdIR8JRCwFjm7mLvATj7i1Q1PPrAST6cxkM74a3syp0x8jPkJIPWsdHT3IQQNwtTdtQUGP+bVzuZpsj+EMxO8B2YZyEpBAU6EhSsfeKOqyZOv9Qy8a6bA46n22XFxHNZWhvg0sG+NgQIhUhhq+Ve6Ip9lVSjKQPBloZBq0jIZxfKNlRxnIEjAPIMJjG9hmvRrsESG0p3Bo5OMqkTpsKHn5VzNbcVVmVIQSh/Jj2mybMwda2X+yALf7il8qZa/icueYs4IaTP7DrKDe872Bv6tKWtJIRC+eIz/fHojWX+2F7A1NnNtd3liwu435aw0ERb1pHlmVj64muXylMpYA0aJZ3BOYJ7xh0BR/lV3ChojyjSwna0R09UdsyGbDZrg16ZE4hxbBiF9EeAn7Vr2TJeesvP1yaFfHI0QYgy964NCU2t/yj4Md3ob/gqGi3SwvJbVvscHfZcOUyJaWJJPGZTMIivhpecJ1ksP6G5FCmNkFIK2oDfswPLaC6yc9SyHoCIU/K/npE79Fy+0V3SLAbZlepa6EIiAoGxTouVVkRuM9OUZgP96d2Sm26BhF8vmCpZ8L70GoNv1UY5nyxnxfZRV+qsJ46IvEjh/FuZRS5HEoHsbCNF6OfPWE1EoOsBqm7xoQ== anule@codeforamerica.org 2 | -------------------------------------------------------------------------------- /deploy/playbooks/roles/users/pubkeys/ashcampo.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDWgTrDevp8kWduEu/Gee5vp8uSAyEBdcHop8crTXyRO5uytU4+o9rgCkbrpfJaIPxdiRmlX4ebvR+MQ+Hy9QqfiR9XZzqspyN9mxDC7mLA0pdymCHBs4xcvkFjl952gek9E5qhmfTwUQP8lk6kD1CzdTg5VB8seDnP8gRwn0+wlZOhbtaHA4YqO7tvTgFCppsuL/VUu4AFJFIwvW4DlVb2YY1BgbaZk0G9XPcz07VheaOnWMH4L7LECUffdf68+iyXyhEo6CY299Y0X7zvhoORFYEpl+eVcdHXeqoy1cSaaI98PQywkrYFFk6cpIf3AbLbHBJfh+6HE9jMW1wa8azslBn9HYeWr8v8xibolKFzaFov6t4F21/cqZUGd5H+2TRSpUrxvrPdBIblfd/JmXxQpGpqAv35GeiMzMZRahGPzYv9vgbsbRG26Ea4t+Jmkfl1YoEkctRCSyAG5865x9fs40qaHaKsgX8Fm4bj8jGQ8lEOovjzieN8DVJ8tBpryzDuUp4aIVnPI5FE3/x3WFCLdC/COrqVChiuC+rQirAmQnUX6ZGFR44j0ucTk1kG6KOcSsxaGKWgf01kFfSLqfBK2+K3X9tz9UbG3cfw2nRpay/GzJbl3MeZ7ZvUbsP+zKYB6YeE7UHJiK7vewSbWKRUL4SUz4z5cwSZs7EfEN1LcQ== acampo@codeforamerica.org 2 | -------------------------------------------------------------------------------- /deploy/playbooks/roles/users/pubkeys/bensheldon.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDCgovrlBGJ9RuABh5kr2HWw4dPPKmhZ3GAm82dCK66T4cC1A3AE4u3G8n0Mky+ovbztH50tOTPx/GcGooBQzFwkHyIGO3EF4Jq5XBWbiVlz+yRYFIcthFeuBeY0GqdbJ/aVrzPeJD23MgxCJyZcDnsIlEVM6vP7AhGruYZcoZzl6Cnpsab0EljHXybpgrZ0GSsAUvAjnK0QdCiwesm+aQjEw+aFhFa7uh6gMTXa0yOhm7LKliC0COSCS3x7evm1IwRgK/5isJ3gB2wmJ6bpHXsq7ijbIHrrEElwY45M38davnsTi1bQnna2shRArmvdglKZXvl522/r7i1G27BRGi07CYjD3zVFxb2LEn5lFnyVy3Kqky4NCCT4UsNUqwsbrFpMyC3CGehafqFSgCo1mvn29CVNDhliNwQQCpIhLZlqqw7f3KaA3xiZhJ5h87f4/3e6TXM+8VgNcNZSL1yDoETses0IHVM+Yz/uhPdQ7E+gnhf9Ap/XeUZ7EmyeSa8St4kvNrJZLCw6uPl5luXx8jH4QFsOrhCZaUhG3tivpYY20NZu6CS83eT1TH6Lj3PQxxhKqe/seETzTODI8+cyvH3/zmYRkTvsMacBZAmM7WeA8eJKvlxSOOqr+nb8odktNYUwYv/RZuIVTRpX9yJz0DpjoBRRTl9Z4bqRnXaCZpA3w== ben@codeforamerica.org 2 | -------------------------------------------------------------------------------- /deploy/playbooks/roles/users/pubkeys/bgolder.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDhCQXFMrwmbm2XvEyUVSxoZsCtHXer2L62HtDJViDmjhw9j0Po8gYM/Bz8osVnH+0p3myds4eFp5itd35ugIw/6iKv5L7Cf2J3zj7OAwzGayCgNQ6jtDJN5e7l7rsFheTgRull9XlGOnqK5dAI1JTQkl47ftT5pSGGrlCy47rVFKM8ngWTb2XTr3VmpbxHS5AjXnkx1xfVNCwSvM9UInPE8MMk/aNLuMrlFF3+neJO6NeqJzRQcNbnm7nVDxUETv3S3cM3FxYYso3Sh7vQ371wCaal1WhZV4rBEAcgPR/G66RC3JJ0mXrSLlIGgBK9QiMyIgmQm1Sawd/pES3PbAQiqUI/bLA5HvhyLQeXnOZgSY4NdP0vSRzDSnBqL5EIxhXSaPyGgJXDeYHv/7t4qqIYwtBs+p9WMdvukbrKb6ciUfBQ3ZgVkDOF2T5/ORpGtjacxkm410FXbUze2J5r5inlkakEs4vwOYT8t3fgv7ug1nabsRk/GneBn/YQ0T6aDpdCyWnNmkAKYkoHG8AKmY+2WtMce0yGZvJJpM9Lt/taKhp91vG58F3UeqOhuMdWn7sQyLNCZZfEfs7O41m2++k1oshrgBDHqxAWIPs3FxeNS7sgpPYraX6RkvFrbHku2/2AlxE3tO5n7wssiyEHWI2AhClZ8f2Cy58yO5eES6Q3Vw== bgolder@codeforamerica.org 2 | -------------------------------------------------------------------------------- /deploy/playbooks/roles/users/pubkeys/eric.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC5BOJdNlPu1Pl+4PZSR5cAUDEo3EunjREmW/+Ha5xqlaAmGiWwmz9zsXMaXMMcOq22feA9J81bs8TrOcE1Xv1PTyiTzYmsvW7LmQWaLK9LKWTQQQA3PSI01hCtun8I8EQSE7mtid4CDHy03Xwk2n5oGyfDpmzujs/MeeTw8CtCW/IMhtfRCF+vYA8ssxClsVYrU0yvSnI9TTx5C6CHcqJ3tjwmxjvkatwI0oA1D5VhyayEu8igwKjOvliN11AHKFr81ufHWW1G/r6z7SD+sMDqmMQDi4ZiYV7yQnHdZ+b8fcwxabQ00ULFUnOmLLa/FvhrtS07odaQ0dZq4nnTVWGa4R5OXIvnTXIFswhDlTZxaGYcR+pEKj2zzt7/cdiNrBcTj5QKI7aLNuZLhiMp0hFwdVdSgHFxAmYDzn3f8hO5lzl4LdNqr8Vmn9gkZqfPvxRNFyJodv2EnYOV7xIhVjtb4ZXi2jZWZlt0QWH3nvtA89CpVg8/ryNVLSBxytbTvZKdhzf3ZbyZEd0+LKBJCs4rhC0as4O3Ssacc4iWT5RkBIcqwQHbehwVNP7aRcmpDUts5RNXJpcYSHKn20XOFrMSEaRg6dQ9LDVAsYOHqKizyn5blJYT4IeLiDmgakkX3jhIsh7pJJOulj4vU4y7Ml6/ZOEi9LrNFTKefUPy8zxGOw== eric@codeforamerica.org 2 | -------------------------------------------------------------------------------- /deploy/playbooks/roles/users/pubkeys/gwen.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDqVKEXP6NOU/iVdxZyy1tYqUDVuhj3eaodRX4GuB/JbSA3uiggvVs/qUjTvrm1KFSoS+vhqciRy4M3H4uzIqFKau3BL7UlqmwkP/NfpOYrEH90ye57a1Qmoa9QOFWGVDMODT8dqKk9nPlb+nVsTuTzJS01RsUmfnBfPVuqk8IwOrQvvbmgXLJZZHJ2mTly9pTAtAFGShJeRnkGR6PQrtDIENxqk8/8+wTfLoetN4baB4plsXi5XPNllNaKJARvIwSLFWbQHEQL4yiPVydY8p1ylcAaTF0WdtjIUbCEz3Rnl29tD4y+Y17KBT1PXmBM3nQJxTgoz2N4rYRys48rBcaNGP7a3LwbbVfFcqhM7Au8kWChGcZHHTSzZXYHwyiuRFPSpoW8ncfXnk1TH9Hx8EM3L8N9m2u79vWSu2vxHXIvdbLLvABVo5YPpLRKk89T+WMu+6E+pZRGw/DQVQKl609lH8niaccG8/1n4MLrk4G4gmliO90C8ln6jJKFhvObQ+fc3Hn8Z/BKlVVTSfYI2sZQPdow3De7Z0TQlMfVP9jYAX3PqI1zPN4y9IDVgjL3rT3PciQyMOrmtKoQWWLT7x3rR5LeXI40AevyG7A2CSeT/mwEaPRx47RXY4GY5pig7dKfSH0Q2sBjtFlfy46pE2Zt637QgmChCzf+jWb/Cz6BdQ== gwen@codeforamerica.org 2 | -------------------------------------------------------------------------------- /deploy/playbooks/roles/users/pubkeys/mikela.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAfsApsVvt+xfS2jo5ZeSZRc74g+5XI7SEBjojCj5WgdyMx62t059XNUJJXeSk2Zs94zV3pjuKN3PyDHvyIv354beJJVfykarcK+rLRxqQPiVWMIfCIN1JVsaJlz2WOMqsl4A9nh+WAsuQNIIAGCOqvcit3Hb5+qJeJ9mAnfEcuei4xdcaRsrc1kK7xV2tvcUPuxckkl+As3PgX5UR82ADyzZ/pCngsKX5it4xnATxTxAaO5Xy+mzAXMp5BHMyM6vad0QfKPFKRwONla/usvx9bLvGifowXD+Z3AlBMp4EggjfZeesq3XuXO2y4bKpExHLqr2m+fhofOqu/LtzR8ICUlRefZNbJEFf+SNq6Sv+2cRRqjSKqxQPHCFITO5omGgErzdV8evmvuGW5qIzPNiaybcqXaUOV/6WQF5sc3y0eckkDUb05cTkvY2pRI9EnWjzyY7IEBTzsAeFOG9a5vG74WYo3wt4y2dr2HIXViko9jbfq0++4Vl/5n6d7aM8HO3Njt8ermbYx8bU/H/AiICS3a65Ymj6xe1SWAy3TBlvRJOognOCVylmi+WE0n3wRfGCCfTZRr8PJZy9QLzvJsqr1jqZzayIak45naqNsLhbFoHc7AZjpC/aMiF8U21GR5DA7UQGOzLNkQRK+xuTHKW7VQMLEnnKyw8PjmMwq8= mikela@yourstruly 2 | -------------------------------------------------------------------------------- /deploy/playbooks/roles/users/pubkeys/tgrathwell.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCcwJAfXLzns/HjN62EHe8aleR0RPKequ50PnB4PN7tR5P6s5VLCEPhGh1qKY+xZVJoILB+Q6J+ZEuQdzn24HcWVvGd/shOeQ+Bu2CYAvRiVlXNlRVGjGQNTILt52ZAPtOgwUzge3ew4SaajBnRqiOa9SGWTro5jmEzf0nW+PDnKJvEyc+Bu7hriCIIPmc/SddvOIdA2kRJxKffVZV+kF6JF4ep1qff3E0ONl9AvjYs0PKmZ/jBI1mac13LimxGaiW0IAy9i4mSsDXOE5NrrPuAL8zWL0UMhaWUr8LOHKF3j5Abm+52TxbUtLlEIUa1nf+xUggqPx59WHnUkxt+SavXLaRY7SIDC74/mYW6YtjGNWMmJcNlN/Sgob6Qs4NSgtLpmfi/7h3gqoaBt9zQsS3imtiPuwXPFqPUYSAB5EqRU1lOCds7vFBnSgOJxWxQSh6POmWaqBWX6UbA2a1/GaYrA8cc9gDpLE9RLDAqOEwkL43N90wKrzLoqRKAHlMgL/gnU7XnrH21XTvrYBgCIMcRlM0CUV3ifd98EjPSj/EBM6RsD5/8d1B5HkiuAX1ie/aDVLKm392w40N09p0nBXINwMc3HN/tHeLmbq64ronZTeiNvTv5UvwdHPoUdw5SCL/cwEEZulMkNM/N9EbUWrQza3FPHD8O+i9mSqP4KKujBw== tgrathwell@codeforamerica.org 2 | -------------------------------------------------------------------------------- /deploy/playbooks/roles/users/pubkeys/tomas.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDH7E+Xd9Zk0vkj1aViSdHbfG8OEQ8PoRTEMgznuyCfGj2bUAqpw5LtVoN/TYpQ8pQdGi9GeCm6a5fiHY3RcgwOz7P+JZOwKf0FcUTnL/6ZDp2x4EoOp/e+2M+XtrXj4xEqMfq8tuKpjIqRB9/ptVnk2/q0ag+9+/ir/ZEAEPKw5TUxwgn6t6otVFlz3yiV+PNclkfOkJdU06wx61fFOGlcY54Y1JZ/kqb9Spx2ep3fee/bWWE1m4cQwCZCQ13bG2z6//90MKOd/viSkpDL6gT9YM7lE029TDFRCYdWUuMBEs+C0dwOcY8AADW+bDzftIPjvJyWrgzk5HzLCcO0tdDXIwnJ6YkKDCKLZ6r3jfSBJBKP7Phdz9Y54l5MwMLsRZ/hCKMX59D8Lv0lkOl0ZowPYvSGfW/QEsnlkMokR78qRcW/Iyj6Z4OGK432jUzNFwng17vFgBvazAiQKWy/hXXKcUPJeaKWwc1q4sdB0GVsPGUqAmkVSonOoJnrX630nP/WuDQE6Rw4dIm1ZUbES6KJ4zTU9u86/646ACRqUU26xR46kwDwO0mr1VtvTm054j9cGWqu58qOldvoHVY2QyRFf//SKuTCyZWcWtviC/UyGclhlvs1+hYWGa44HoMarCKjU8+DuQHt7nw4/YzKWGjtMHrGs5YnPoIDzvwH4Altaw== tomas@codeforamerica.org 2 | -------------------------------------------------------------------------------- /deploy/playbooks/roles/users/pubkeys/zak.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDlgG+Yepuof2fQwAQHK8Nc/fIddk3oTJTONXvGx0O0f3hKid31rsAPqM7+nIKYXL4fb9QVdWOyi0a/AquOms4yPFbDFFTKH/uCPaEAxu338lGtbuCEvvUMF48p5cZvXc0ELVOtag7VJ+RCTcyv2WOW8c7tZuyUM/IZjnq4KWVv25TtBOwmOxk0xiQop9Y1lMyS+VQSmSTUCoIc/lF6YRQ3t9yb5pMgQ5UKyPJupWxu1PTkOiO08+7DgOlRNpvv/LdyGnP6q5m5XQPXHKmpSQUKZ7hHmDYzUXrkhYBIm2sUiFITBaILUvJCCeNbAHJKU9X5mqw/BMchfVi7E0EHNNAD zak@codeforamerica.org 2 | -------------------------------------------------------------------------------- /deploy/playbooks/roles/users/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: ensure that all members of sudo are nopasswd 3 | lineinfile: dest=/etc/sudoers.d/30-sudo_group owner=root group=root mode=0440 line="%sudo ALL=(ALL) NOPASSWD:ALL" state=present create=yes validate='visudo -cf %s' 4 | - name: "ensure users have an account" 5 | user: 6 | name: "{{ item.name }}" 7 | comment: "{{ item.comment }}" 8 | groups: admin,sudo 9 | update_password: on_create 10 | shell: /bin/bash 11 | remove: yes 12 | state: "{{ item.state }}" 13 | with_items: 14 | - "{{ users }}" 15 | - name: "ensure users have an authorized key" 16 | authorized_key: 17 | user: "{{ item.name }}" 18 | key: "{{ lookup('file', './pubkeys/' + item.name + '.pub') }}" 19 | state: "{{ item.state }}" 20 | when: item.state == "present" 21 | with_items: 22 | - "{{ users }}" 23 | -------------------------------------------------------------------------------- /deploy/terraform/app/apply.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | APPLY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | 5 | terraform apply \ 6 | -var-file=<(lpass show --notes "${1}") \ 7 | ${APPLY_DIR} 8 | -------------------------------------------------------------------------------- /deploy/terraform/app/plan.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | APPLY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | 5 | terraform plan \ 6 | -var-file=<(lpass show --notes "${1}") \ 7 | ${APPLY_DIR} 8 | -------------------------------------------------------------------------------- /deploy/terraform/apply.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | terraform get 3 | 4 | APPLY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 5 | 6 | terraform apply \ 7 | -var-file=<(lpass show --notes "${1}") \ 8 | ${APPLY_DIR} 9 | -------------------------------------------------------------------------------- /deploy/terraform/email/apply.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | APPLY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | 5 | terraform apply \ 6 | -var-file=<(lpass show --notes "${1}") \ 7 | ${APPLY_DIR} 8 | 9 | -------------------------------------------------------------------------------- /deploy/terraform/email/plan.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | APPLY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | 5 | terraform plan \ 6 | -var-file=<(lpass show --notes "${1}") \ 7 | ${APPLY_DIR} 8 | 9 | -------------------------------------------------------------------------------- /deploy/terraform/plan.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | terraform get 3 | 4 | APPLY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 5 | 6 | terraform plan \ 7 | -var-file=<(lpass show --notes "${1}") \ 8 | ${APPLY_DIR} 9 | -------------------------------------------------------------------------------- /heroku_run_on_all_deploys: -------------------------------------------------------------------------------- 1 | command="echo '$1' |rails c" 2 | deploys=('try' 'demo' 'slco' '5cbc' 'danecrc' 'pima' 'baltimore' 'cccounty' 'georgiadcs' 'multco') 3 | for deploy in "${deploys[@]}" 4 | do 5 | heroku run $command --app clientcomm-$deploy 6 | done 7 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/lib/assets/.keep -------------------------------------------------------------------------------- /lib/scripts/copy_database: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pg_dump $DATABASE_URL > /tmp/backup 4 | psql $MIGRATION_TEST_DATABASE_URL < /tmp/backup 5 | -------------------------------------------------------------------------------- /lib/scripts/heroku_run_copy_database: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | heroku pg:reset --app clientcomm-migration-test --confirm clientcomm-migration-test 4 | heroku run ./lib/scripts/copy_database --app $1 5 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/lib/tasks/.keep -------------------------------------------------------------------------------- /lib/tasks/delayed_cron_job.rake: -------------------------------------------------------------------------------- 1 | namespace :delayed_cron_job do 2 | task reset: :environment do 3 | Delayed::Job.where.not(cron: nil).destroy_all 4 | DeadManSwitchJob.perform_later 5 | ScheduledMessageCronJob.set(cron: "*/#{APP_CONFIG['scheduled_message_rate']} * * * *").perform_later 6 | DeadManSwitchJob.set(cron: '* * * * *').perform_later 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/tasks/notes_to_id_number.rake: -------------------------------------------------------------------------------- 1 | namespace :migrations do 2 | task notes_to_id_number: :environment do 3 | rrs = ReportingRelationship.where.not(notes: [nil, '']) 4 | if rrs.pluck(:client_id).length != rrs.pluck(:client_id).uniq.length 5 | puts 'Warning conflicting RRS for a client' 6 | return 7 | end 8 | rrs.each do |rr| 9 | puts rr.notes 10 | ctrack = rr.notes.scan(/\d+/).first 11 | rr.client.update!(id_number: ctrack) if ctrack 12 | puts rr.client.id_number 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/tasks/reports.rake: -------------------------------------------------------------------------------- 1 | require 'csv' 2 | 3 | namespace :reports do 4 | task generate_and_send_reports: :environment do 5 | end_date = Time.zone.now 6 | if end_date.wday.to_s == ENV['REPORT_DAY'] 7 | Department.all.each do |department| 8 | recipients = department.reports.pluck(:email) 9 | metrics = department.message_metrics(end_date) 10 | recipients.each do |recipient| 11 | NotificationMailer.report_usage(recipient, metrics, end_date.to_s) 12 | .deliver_later 13 | end 14 | end 15 | end 16 | end 17 | 18 | task long_messages: :environment do 19 | io = $stdout.dup 20 | CSV(io) do |csv| 21 | csv << %w[name email client id length send_at] 22 | Message.messages.where('length(body) > 1600').find_each do |msg| 23 | csv << [msg.user.full_name, msg.user.email, msg.client.full_name, msg.id, msg.body.length, msg.send_at.strftime('%Y-%m-%d %H:%M:%S %Z')] 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/tasks/surveys.rake: -------------------------------------------------------------------------------- 1 | namespace :surveys do 2 | task scheduled_messages: :environment do 3 | users = User.select('users.id, users.full_name, users.email, messages.send_at, left(messages.body, 20) as body') 4 | .joins(:messages).where("messages.send_at - messages.created_at > '1 hour'::interval") 5 | .where(messages: { send_at: (Time.zone.now - 48.hours)..(Time.zone.now - 24.hours) }) 6 | .where(messages: { sent: true }) 7 | 8 | users.each do |user| 9 | puts '' 10 | puts "#{user[:full_name]} <#{user[:email]}>" 11 | puts "message: #{user[:body]}..." 12 | puts "on: #{user[:send_at].in_time_zone(Time.zone).strftime('%A, %B %-d, %-l:%M%p; %Z')}" 13 | puts '################################' 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/clientcomm_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/public/clientcomm_01.png -------------------------------------------------------------------------------- /public/clientcomm_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/public/clientcomm_02.png -------------------------------------------------------------------------------- /public/clientcomm_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/public/clientcomm_03.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /public/twilio_alert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/public/twilio_alert.png -------------------------------------------------------------------------------- /public/twilio_deploy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/public/twilio_deploy.png -------------------------------------------------------------------------------- /public/twilio_develop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/public/twilio_develop.png -------------------------------------------------------------------------------- /release-tasks.sh: -------------------------------------------------------------------------------- 1 | rake db:migrate 2 | rake delayed_cron_job:reset 3 | -------------------------------------------------------------------------------- /spec/channels/clients_channel_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe ClientsChannel, type: :channel do 4 | let(:user) { create :user } 5 | 6 | before do 7 | stub_connection current_user: user 8 | end 9 | 10 | it 'subscribes' do 11 | subscribe 12 | 13 | expect(subscription).to be_confirmed 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/channels/messages_channel_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe MessagesChannel, type: :channel do 4 | let(:user) { create :user } 5 | let(:client) { create :client, user: user } 6 | 7 | before do 8 | stub_connection current_user: user 9 | end 10 | 11 | subject { subscribe(client_id: client.id) } 12 | 13 | it 'subscribes' do 14 | subject 15 | 16 | expect(subscription).to be_confirmed 17 | end 18 | 19 | context 'accessing clients that do not belong to the current_user' do 20 | let(:user2) { create :user } 21 | 22 | before do 23 | stub_connection current_user: user2 24 | end 25 | 26 | it 'rejects the subscription' do 27 | subject 28 | 29 | expect(subscription).to be_rejected 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/channels/scheduled_messages_channel_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe ScheduledMessagesChannel, type: :channel do 4 | let(:user) { create :user } 5 | let(:client) { create :client, user: user } 6 | 7 | before do 8 | stub_connection current_user: user 9 | end 10 | 11 | subject { subscribe(client_id: client.id) } 12 | 13 | it 'subscribes' do 14 | subject 15 | 16 | expect(subscription).to be_confirmed 17 | end 18 | 19 | context 'accessing clients that do not belong to the current_user' do 20 | let(:user2) { create :user } 21 | 22 | before do 23 | stub_connection current_user: user2 24 | end 25 | 26 | it 'rejects the subscription' do 27 | subject 28 | 29 | expect(subscription).to be_rejected 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/factories/attachments.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :attachment do 3 | message { build :text_message, inbound: true } 4 | media { Rack::Test::UploadedFile.new(Rails.root.join('spec', 'fixtures', 'fluffy_cat.jpg'), 'image/png') } 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/factories/change_image.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :change_image do 3 | file { Rack::Test::UploadedFile.new(Rails.root.join('spec', 'fixtures', 'fluffy_cat.jpg'), 'image/png') } 4 | user { create :user } 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/factories/client_edit_marker.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :client_edit_marker do 3 | reporting_relationship { create :reporting_relationship } 4 | body { "i am a client edit marker #{SecureRandom.hex(17)}" } 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/factories/client_statuses.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :client_status do 3 | sequence(:name) { Faker::Lorem.unique.word } 4 | followup_date { nil } 5 | icon_color { "\##{SecureRandom.hex(3)}" } 6 | department do 7 | create :department 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/factories/conversation_ends_marker.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :conversation_ends_marker do 3 | reporting_relationship { create :reporting_relationship } 4 | body { "i am a 'conversation ends' marker #{SecureRandom.hex(17)}" } 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/factories/court_date_csvs.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :court_date_csv do 3 | file { Rack::Test::UploadedFile.new(Rails.root.join('spec', 'fixtures', 'court_dates.csv'), 'text/csv') } 4 | user { create :user } 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/factories/court_reminders.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :court_reminder do 3 | reporting_relationship { create :reporting_relationship } 4 | body { "i am a court reminder #{SecureRandom.hex(17)}" } 5 | court_date_csv { create :court_date_csv } 6 | send_at { Time.zone.now + 2.days } 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/factories/departments.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :department do 3 | sequence(:name) { |n| "Department#{n}" } 4 | phone_number { "+1760555#{Faker::PhoneNumber.unique.subscriber_number}" } 5 | 6 | after(:create) do |dept, evaluator| 7 | unless evaluator.unclaimed_user 8 | dept.update!(unclaimed_user: create(:user, phone_number: nil, full_name: 'Unclaimed', department: dept)) 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/factories/highlight_blobs.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :highlight_blob do 3 | text 'MyText' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/factories/merged_with_marker.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :merged_with_marker do 3 | reporting_relationship { create :reporting_relationship } 4 | body { "i am a 'merged with' marker #{SecureRandom.hex(17)}" } 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/factories/reporting_relationships.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :reporting_relationship do 3 | user { create :user } 4 | client { create :client } 5 | category { ReportingRelationship::CATEGORIES.keys.first } 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/factories/reports.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :report do 3 | email { Faker::Internet.unique.email } 4 | department nil 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/factories/survey_questions.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :survey_question do 3 | text { Faker::Lorem.sentence } 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/factories/survey_response_links.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :survey_response_link do 3 | survey nil 4 | survey_response nil 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/factories/survey_responses.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :survey_response do 3 | text { Faker::Lorem.sentence } 4 | survey_question nil 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/factories/surveys.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :survey do 3 | client nil 4 | user nil 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/factories/templates.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :template do 3 | user { create :user } 4 | sequence(:title) { Faker::Lorem.sentence(4, false, 0) } 5 | sequence(:body) { Faker::Lorem.sentence } 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/factories/text_message.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :text_message do 3 | reporting_relationship { create :reporting_relationship } 4 | body { "i am a message #{SecureRandom.hex(17)}" } 5 | inbound { [true, false].sample } 6 | sequence(:twilio_sid) { |n| (n.to_s + SecureRandom.hex(17))[0..33] } 7 | twilio_status { inbound ? %w[receiving received].sample : %w[accepted queued sending sent delivered undelivered failed].sample } 8 | send_at { Time.current } 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/factories/transfer_marker.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :transfer_marker do 3 | reporting_relationship { create :reporting_relationship } 4 | body { "i am a transfer marker #{SecureRandom.hex(17)}" } 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/factories/users.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :user do 3 | email { Faker::Internet.unique.email } 4 | password { Faker::Internet.unique.password } 5 | full_name { Faker::Name.name } 6 | phone_number { "+1760555#{Faker::PhoneNumber.unique.subscriber_number}" } 7 | 8 | transient do 9 | dept_phone_number { "+1760555#{Faker::PhoneNumber.unique.subscriber_number}" } 10 | end 11 | 12 | department do 13 | create :department, phone_number: dept_phone_number 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/fake_twilio_client.rb: -------------------------------------------------------------------------------- 1 | require 'cgi' 2 | 3 | class FakeTwilioClient 4 | # source: https://robots.thoughtbot.com/testing-sms-interactions 5 | FakeResponse = Struct.new(:sid, :status) 6 | 7 | def initialize(_account_sid, _auth_token) 8 | self 9 | end 10 | 11 | def api 12 | self 13 | end 14 | 15 | def messages(_sid = nil) 16 | self 17 | end 18 | 19 | def num_media 20 | '0' 21 | end 22 | 23 | def media 24 | self 25 | end 26 | 27 | def list 28 | [] 29 | end 30 | 31 | def account 32 | self 33 | end 34 | 35 | def fetch(_params = nil) 36 | self 37 | end 38 | 39 | def update(_params) 40 | nil 41 | end 42 | 43 | def lookups 44 | self 45 | end 46 | 47 | def v1 48 | self 49 | end 50 | 51 | def phone_numbers(phone_number) 52 | @phone_number = CGI.unescape(phone_number) 53 | self 54 | end 55 | 56 | def phone_number 57 | @phone_number 58 | end 59 | 60 | # rubocop:disable Lint/UnusedMethodArgument 61 | def create(**kwargs) 62 | FakeResponse.new(SecureRandom.hex(17), 'delivered') 63 | end 64 | # rubocop:enable Lint/UnusedMethodArgument 65 | end 66 | -------------------------------------------------------------------------------- /spec/features/feature_flags_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | feature 'feature flags' do 4 | describe 'mass messages' do 5 | let(:myuser) { create :user } 6 | 7 | before do 8 | login_as(myuser, scope: :user) 9 | end 10 | 11 | context 'enabled' do 12 | before do 13 | FeatureFlag.create!(flag: 'mass_messages', enabled: true) 14 | end 15 | 16 | it 'shows mass messages button' do 17 | visit clients_path 18 | expect(page).to have_content 'Mass message' 19 | end 20 | end 21 | 22 | context 'disabled' do 23 | it 'does not show mass messages button' do 24 | visit clients_path 25 | expect(page).not_to have_content 'Mass message' 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/features/user_sends_message_that_errors_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | feature 'receiving messages', active_job: true do 4 | let(:message_body) { 'You have an appointment tomorrow at 10am' } 5 | let(:client_1) { create :client, users: [myuser] } 6 | let(:myuser) { create :user } 7 | let(:rr) { myuser.reporting_relationships.find_by(client: client_1) } 8 | 9 | before do 10 | login_as(myuser, scope: :user) 11 | end 12 | 13 | scenario 'client has blacklisted messages from clientcomm', :js, active_job: true do 14 | step 'when the client has texted stop' do 15 | visit reporting_relationship_path(rr) 16 | 17 | error = Twilio::REST::RestError.new('Blacklisted', 21610, 403) 18 | expect_any_instance_of(FakeTwilioClient).to receive(:create).and_raise(error) 19 | 20 | fill_in 'Send a text message', with: message_body 21 | 22 | perform_enqueued_jobs do 23 | click_on 'send_message' 24 | expect(page).to have_content message_body 25 | expect(page).to have_css '.blacklisted', text: I18n.t('message.status.blacklisted') 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/features/user_views_court_reminder_on_conversation_page_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | feature 'creating and editing scheduled messages', active_job: true do 4 | let(:userone) { create :user } 5 | let(:clientone) { create :client, user: userone } 6 | let(:rrone) { ReportingRelationship.find_by(client: clientone, user: userone) } 7 | let(:past_date) { Time.zone.now - 1.month } 8 | let!(:reminder) { create :court_reminder, reporting_relationship: rrone, send_at: past_date } 9 | scenario 'user views court reminder', :js do 10 | step 'when user logs in' do 11 | login_as(userone, scope: :user) 12 | end 13 | 14 | step 'when user goes to messages page' do 15 | visit reporting_relationship_path(rrone) 16 | expect(page).to have_content reminder.body 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/fixtures/bad_court_dates.csv: -------------------------------------------------------------------------------- 1 | ofndr_num,(expression),lname,crt_dt,crt_tm,crt_rm 2 | 111,1873D,BROCK, not 5/8/2018,8:30,37 3 | 202,1867J,SABRINA,5/16/2018,9:00,5 4 | 202,1868D,MISTY,5/8/2018,14:00,N45 5 | 321,1868D,BLAINE,5/8/2018,14:00,MH 6 | 867,1890J,NORMAN,5/22/2018,10:00,1 7 | -------------------------------------------------------------------------------- /spec/fixtures/cat_contact.vcf: -------------------------------------------------------------------------------- 1 | BEGIN:VCARD 2 | N:Snowball;Lumpy;;Cat Contact; 3 | ADR;INTL;PARCEL;WORK:;;1 Main St.;Cat Town;Catafornia;11111;USA 4 | EMAIL;INTERNET:lumpy.snowball@gmail.com 5 | END:VCARD 6 | -------------------------------------------------------------------------------- /spec/fixtures/court_dates.csv: -------------------------------------------------------------------------------- 1 | ofndr_num,(expression),lname,crt_dt,crt_tm,crt_rm 2 | 111,1873D,BROCK,5/8/2018,8:30,37 3 | 202,1867J,SABRINA,5/16/2018,9:00,5 4 | 202,1868D,MISTY,5/8/2018,14:00,N45 5 | 321,1868D,BLAINE,5/8/2018,14:00,MH 6 | 867,1890J,NORMAN,5/22/2018,10:00,1 7 | -------------------------------------------------------------------------------- /spec/fixtures/court_locs.csv: -------------------------------------------------------------------------------- 1 | crt_loc_cd,crt_loc_desc,vld_flg 2 | 1873D,LITTLEROOT TOWN,Y 3 | 1867J,PEWTER CITY,Y 4 | 1868D,GOLDENROD COURT,Y 5 | 1890J,FORTREE JUSTICE,Y 6 | -------------------------------------------------------------------------------- /spec/fixtures/fluffy_cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/spec/fixtures/fluffy_cat.jpg -------------------------------------------------------------------------------- /spec/fixtures/large_image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/spec/fixtures/large_image.jpg -------------------------------------------------------------------------------- /spec/fixtures/test_import_one_client.csv: -------------------------------------------------------------------------------- 1 | last_name,first_name,phone_number,user 2 | McCatFace,Cat,12125556230,test@example.com 3 | -------------------------------------------------------------------------------- /spec/fixtures/test_import_two_clients.csv: -------------------------------------------------------------------------------- 1 | last_name,first_name,phone_number,user 2 | McPersonFace,Person,14155553329,test@example.com 3 | McCatFace,Cat,12125556230,test@example.com 4 | -------------------------------------------------------------------------------- /spec/javascripts/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/spec/javascripts/helpers/.gitkeep -------------------------------------------------------------------------------- /spec/lib/voice_service_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe VoiceService do 4 | describe '#generate_text_response' do 5 | it 'takes a string and responds with twiml' do 6 | response = subject.generate_text_response(message: 'Hello there') 7 | expect(response).to eq 'Hello there' 8 | end 9 | end 10 | 11 | describe '#dial_number' do 12 | it 'takes a phone number and responds with twiml to dial that number' do 13 | response = subject.dial_number(phone_number: '+12425551212') 14 | expect(response).to eq '+12425551212' 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/models/change_image_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe ChangeImage, type: :model do 4 | describe 'validations' do 5 | it { should belong_to :user } 6 | 7 | it { 8 | should validate_attachment_content_type(:file) 9 | .allowing('image/png') 10 | .rejecting('text/whatever') 11 | } 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/models/client_status_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe ClientStatus, type: :model do 4 | it { should belong_to :department } 5 | it { should validate_presence_of :name } 6 | it { should validate_presence_of :icon_color } 7 | end 8 | -------------------------------------------------------------------------------- /spec/models/court_date_csv_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe CourtDateCSV, type: :model do 4 | describe 'validations' do 5 | it { should belong_to :user } 6 | 7 | it { 8 | should validate_attachment_content_type(:file) 9 | .allowing('text/csv') 10 | .rejecting('png/whatever') 11 | } 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/models/court_reminder_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe CourtReminder, type: :model do 4 | it { should belong_to :court_date_csv } 5 | # it { should validate_presence_of :court_date_csv } 6 | end 7 | -------------------------------------------------------------------------------- /spec/models/highlight_blob_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe HighlightBlob, type: :model 4 | -------------------------------------------------------------------------------- /spec/models/report_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Report, type: :model do 4 | it { should belong_to :department } 5 | 6 | describe 'validations' do 7 | it { should validate_presence_of :email } 8 | end 9 | 10 | describe 'users' do 11 | let(:department) { create :department } 12 | let(:active_users) { create_list :user, 5, department: department } 13 | let(:inactive_users) { create_list :user, 5, department: department, active: false } 14 | let(:report) { create :report, department: department } 15 | 16 | it 'returns active users' do 17 | active_users << department.unclaimed_user 18 | expect(report.users).to match_array(active_users) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/models/survey_question_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe SurveyQuestion, type: :model do 4 | it { should have_many :survey_responses } 5 | end 6 | -------------------------------------------------------------------------------- /spec/models/survey_response_link_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe SurveyResponseLink, type: :model do 4 | it { should belong_to :survey } 5 | it { should belong_to :survey_response } 6 | end 7 | -------------------------------------------------------------------------------- /spec/models/survey_response_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe SurveyResponse, type: :model do 4 | it { should belong_to(:survey_question) } 5 | it { should have_many(:survey_response_links) } 6 | end 7 | -------------------------------------------------------------------------------- /spec/models/survey_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Survey, type: :model do 4 | let!(:user) { create :user } 5 | let!(:client) { create :client, user: user } 6 | let!(:survey) { create :survey, user: user, client: client } 7 | 8 | it { should belong_to :user } 9 | it { should belong_to :client } 10 | it { should have_many(:survey_responses).through(:survey_response_links) } 11 | 12 | describe '#questions' do 13 | before do 14 | create_list :survey_question, 2 do |question| 15 | survey.survey_responses << create_list(:survey_response, 3, survey_question: question) 16 | end 17 | end 18 | 19 | it 'returns only the unique questions for the survey' do 20 | expect(survey.questions.count).to eq(2) 21 | expect(survey.questions).to contain_exactly(SurveyQuestion.first, SurveyQuestion.last) 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/requests/admin/department_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe 'Department', type: :request, active_job: true do 4 | let(:department_name) { 'Main' } 5 | let(:department_phone_number) { '+14155551212' } 6 | let(:admin_user) { create :user, admin: true } 7 | 8 | before do 9 | login_as admin_user 10 | end 11 | 12 | describe 'POST#create' do 13 | context 'with valid parameters' do 14 | it 'creates a department' do 15 | post admin_departments_path params: { 16 | department: { 17 | name: department_name, 18 | phone_number: department_phone_number 19 | } 20 | } 21 | 22 | new_department = Department.last 23 | expect(new_department.phone_number).to eq(department_phone_number) 24 | expect(new_department.name).to eq(department_name) 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/requests/help_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe 'Help requests', type: :request do 4 | context 'unauthenticated' do 5 | let(:help_link) { 'help me omg' } 6 | 7 | before do 8 | @help_link = ENV['HELP_LINK'] 9 | ENV['HELP_LINK'] = help_link 10 | end 11 | 12 | after { ENV['HELP_LINK'] = @help_link } 13 | 14 | subject { get help_index_path } 15 | 16 | it 'redirects the user to the github help page' do 17 | subject 18 | expect_analytics_events_happened('help_page_click') 19 | expect(response).to redirect_to help_link 20 | end 21 | 22 | context 'no help page is set' do 23 | let(:help_link) { '' } 24 | 25 | it 'raises a 404 error' do 26 | expect { subject }.to raise_error(ActionController::RoutingError) 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/requests/layout_request_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe 'Layouts', type: :request do 4 | describe 'Navigation bar' do 5 | context 'help link' do 6 | let(:help_link) { 'help me omg' } 7 | 8 | before do 9 | @help_link = ENV['HELP_LINK'] 10 | ENV['HELP_LINK'] = help_link 11 | 12 | sign_in create :user 13 | end 14 | 15 | after { ENV['HELP_LINK'] = @help_link } 16 | 17 | subject { get root_path } 18 | 19 | it 'displays a help link' do 20 | subject 21 | 22 | expect(response.body).to include 'Help' 23 | end 24 | 25 | context 'there is no help link set' do 26 | let(:help_link) { nil } 27 | 28 | it 'does not display a help link' do 29 | subject 30 | 31 | expect(response.body).to_not include 'Help' 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/requests/login_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe 'Root Paths', type: :request do 4 | subject { get root_path } 5 | 6 | context 'Unauthenticated' do 7 | it 'shows the login page' do 8 | subject 9 | expect(response.body).to include('Log in') 10 | end 11 | end 12 | 13 | context 'Disabled' do 14 | let(:inactive_user) { create :user, active: false } 15 | 16 | it 'shows the login page' do 17 | sign_in inactive_user 18 | 19 | subject 20 | 21 | expect(response.body).to include('Log in') 22 | expect(response.body).to include('Sorry, this account has been disabled.') 23 | end 24 | end 25 | 26 | context 'Authenticated' do 27 | let(:user) { create :user, dept_phone_number: '+12431551212' } 28 | 29 | it 'shows the clients index page' do 30 | sign_in user 31 | 32 | subject 33 | 34 | expect(response.body).to include('My clients') 35 | expect(response.body).to include('(243) 155-1212') 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/requests/messages_analytics_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe 'Tracking of message analytics events', type: :request do 4 | context 'GET#index' do 5 | it 'tracks a visit to the message index' do 6 | user = create :user 7 | sign_in user 8 | client = create :client, user: user 9 | rr = user.reporting_relationships.find_by(client: client) 10 | get reporting_relationship_path(rr) 11 | expect(response.code).to eq '200' 12 | 13 | expect_analytics_events('client_messages_view' => { 14 | 'client_id' => client.id, 15 | 'has_unread_messages' => false, 16 | 'hours_since_contact' => 0, 17 | 'messages_all_count' => 0, 18 | 'messages_received_count' => 0, 19 | 'messages_sent_count' => 0, 20 | 'messages_attachments_count' => 0 21 | }) 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/requests/registrations_analytics_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe 'RegistrationsController', type: :request do 4 | describe 'GET#new' do 5 | it 'redirects to the sign in page' do 6 | get new_user_registration_path 7 | expect(response).to redirect_to '/users/sign_in' 8 | end 9 | end 10 | 11 | describe 'POST#create' do 12 | it 'redirects to the sign in page' do 13 | post user_registration_path 14 | expect(response).to redirect_to '/users/sign_in' 15 | expect(flash[:notice]).to include('Contact an administrator') 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/requests/sessions_analytics_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe 'Tracking of session analytics events', type: :request do 4 | context 'GET#new' do 5 | it 'tracks a visit to the log in page' do 6 | get new_user_session_path 7 | expect(response.code).to eq '200' 8 | expect_analytics_events_happened('login_view') 9 | end 10 | end 11 | 12 | context 'POST#resource' do 13 | it 'tracks a user successfully logging in' do 14 | userone = create :user 15 | sign_in userone 16 | expect_analytics_events_happened('login_success') 17 | end 18 | 19 | it 'tracks a user erroring when trying to log in' do 20 | # an unsaved user shouldn't be able to log in 21 | userone = build :user 22 | sign_in userone 23 | expect_analytics_events_happened('login_error') 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/requests/welcomes_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe 'Reporting Relationship Welcome Requests', type: :request, active_job: true do 4 | include ActiveJob::TestHelper 5 | 6 | let(:user) { create :user } 7 | let!(:client) { create :client, user: user } 8 | let(:rr) { ReportingRelationship.find_by(user: user, client: client) } 9 | 10 | context 'unauthenticated' do 11 | it 'rejects unauthenticated user' do 12 | get new_reporting_relationship_welcome_path rr 13 | expect(response.code).to eq '302' 14 | expect(response).to redirect_to new_user_session_path 15 | end 16 | end 17 | 18 | context 'authenticated' do 19 | before do 20 | sign_in user 21 | end 22 | 23 | describe 'GET#new' do 24 | subject { get new_reporting_relationship_welcome_path(rr) } 25 | 26 | it 'tracks a visit to the welcome form' do 27 | subject 28 | expect(response.code).to eq '200' 29 | expect_analytics_events( 30 | 'welcome_prompt_view' => { 31 | 'client_id' => client.id 32 | } 33 | ) 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/support/active_job.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.include ActiveJob::TestHelper, active_job: true 3 | 4 | config.before(:each) do |example| 5 | next if example.metadata[:type] != :job || example.metadata[:active_job].blank? 6 | clear_enqueued_jobs 7 | clear_performed_jobs 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/support/database_cleaner.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.use_transactional_fixtures = false 3 | 4 | config.before(:suite) do 5 | DatabaseCleaner.clean_with(:truncation) 6 | end 7 | 8 | config.before(:each) do |example| 9 | DatabaseCleaner.strategy = example.metadata[:js] ? :truncation : :transaction 10 | DatabaseCleaner.start 11 | end 12 | 13 | config.after(:each) do 14 | DatabaseCleaner.clean 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/support/match_html.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :match_html do |expected| 2 | def clean_html(s) 3 | s = s.squish.gsub(/>(\s)+<') 4 | Nokogiri::HTML.fragment(s).to_xhtml(indent: 2, save_options: Nokogiri::XML::Node::SaveOptions::AS_HTML) 5 | end 6 | 7 | match do |actual| 8 | @actual = clean_html(actual) 9 | @expected = clean_html(expected) 10 | values_match? @expected, @actual 11 | end 12 | 13 | failure_message do |_actual| 14 | diff = RSpec::Support::Differ.new.diff_as_string(@actual, @expected) 15 | "HTML did not match.\nACTUAL:\n#{@actual}\n\nEXPECTED:\n#{@expected}\n\nDIFF:#{diff}" 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/support/request_helper.rb: -------------------------------------------------------------------------------- 1 | module RequestHelper 2 | def sign_in(user) 3 | post_params = { user: { email: user.email, password: user.password } } 4 | post user_session_path, params: post_params 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/support/responsive_helper.rb: -------------------------------------------------------------------------------- 1 | module ResponsiveHelper 2 | def resize_window_to_mobile 3 | resize_window_by([640, 480]) 4 | end 5 | 6 | def resize_window_to_tablet 7 | resize_window_by([960, 640]) 8 | end 9 | 10 | def resize_window_to_default 11 | resize_window_by([1024, 768]) 12 | end 13 | 14 | private 15 | 16 | def resize_window_by(size) 17 | Capybara.page.driver.browser.manage.window.resize_to(size[0], size[1]) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/support/wait_for_ajax.rb: -------------------------------------------------------------------------------- 1 | module WaitForAjax 2 | def wait_for_ajax 3 | wait_for 'all ajax requests to complete' do 4 | finished_all_ajax_requests? 5 | end 6 | end 7 | 8 | private 9 | 10 | def finished_all_ajax_requests? 11 | page.evaluate_script('window.$ !== undefined') && 12 | page.evaluate_script('$.active').zero? 13 | end 14 | end 15 | 16 | RSpec.configure do |config| 17 | config.include WaitForAjax 18 | end 19 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/vendor/assets/javascripts/.keep -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/clientcomm-rails/3bb430ac6c323bb8d28b1407e769711fc9f4ea0f/vendor/assets/stylesheets/.keep --------------------------------------------------------------------------------