├── app
├── mailers
│ ├── .keep
│ ├── custom_devise_mailer.rb
│ └── mailer.rb
├── models
│ ├── .keep
│ ├── concerns
│ │ └── .keep
│ ├── category.rb
│ ├── department.rb
│ ├── audit.rb
│ ├── saved_search.rb
│ ├── attachment.rb
│ └── question.rb
├── assets
│ ├── images
│ │ ├── .keep
│ │ └── hero.jpg
│ ├── javascripts
│ │ ├── sections
│ │ │ ├── .keep
│ │ │ └── opportunities
│ │ │ │ ├── edit.coffee
│ │ │ │ └── index.coffee
│ │ ├── components
│ │ │ ├── .keep
│ │ │ ├── selectize.coffee
│ │ │ ├── data_toggle_visible.coffee
│ │ │ ├── data_show_if_checked.coffee
│ │ │ ├── data_toggle_text.coffee
│ │ │ ├── data_show_if_selected.coffee
│ │ │ ├── submission_adapters.coffee
│ │ │ ├── datetime_picker.coffee
│ │ │ ├── progress_guard.coffee
│ │ │ └── attachment_upload.coffee
│ │ ├── theme.js
│ │ ├── base.coffee
│ │ └── application.js
│ └── stylesheets
│ │ ├── theme.css
│ │ └── application.css
├── controllers
│ ├── concerns
│ │ └── .keep
│ ├── admin
│ │ ├── users_controller.rb
│ │ ├── questions_controller.rb
│ │ ├── attachments_controller.rb
│ │ ├── categories_controller.rb
│ │ ├── departments_controller.rb
│ │ ├── opportunities_controller.rb
│ │ ├── saved_searches_controller.rb
│ │ └── application_controller.rb
│ ├── static_controller.rb
│ ├── users
│ │ ├── passwords_controller.rb
│ │ ├── sessions_controller.rb
│ │ ├── confirmations_controller.rb
│ │ └── registrations_controller.rb
│ ├── home_controller.rb
│ ├── saved_searches_controller.rb
│ ├── attachments_controller.rb
│ ├── application_controller.rb
│ └── questions_controller.rb
├── jobs
│ ├── application_job.rb
│ ├── queue_user_search_results_job.rb
│ ├── send_deadline_reminders_job.rb
│ └── user_search_results_job.rb
├── views
│ ├── users
│ │ ├── registrations
│ │ │ ├── edit.html.erb
│ │ │ ├── new.html.erb
│ │ │ ├── _business_data.html.erb
│ │ │ ├── confirm.erb
│ │ │ ├── _my_opportunities.erb
│ │ │ ├── _edit_password.html.erb
│ │ │ ├── _saved_searches.html.erb
│ │ │ ├── _new_staff.erb
│ │ │ ├── _new_vendor.html.erb
│ │ │ └── _edit_account.html.erb
│ │ ├── passwords
│ │ │ ├── new.html.erb
│ │ │ └── edit.html.erb
│ │ ├── confirmations
│ │ │ └── new.html.erb
│ │ └── sessions
│ │ │ └── new.html.erb
│ ├── mailer
│ │ ├── _greeting.html.erb
│ │ ├── search_results.html.erb
│ │ ├── question_deadline.html.erb
│ │ ├── submission_deadline.html.erb
│ │ ├── approval_request.html.erb
│ │ ├── question_answered.html.erb
│ │ └── question_asked.html.erb
│ ├── application
│ │ ├── _flashes.html.erb
│ │ ├── _inner_layout.html.erb
│ │ ├── _webfonts.html.erb
│ │ └── _footer.html.erb
│ ├── custom_devise_mailer
│ │ ├── password_change.html.erb
│ │ ├── confirmation_instructions.html.erb
│ │ └── reset_password_instructions.html.erb
│ ├── opportunities
│ │ ├── edit
│ │ │ ├── _contact.html.erb
│ │ │ ├── _submission_adapter_inputs.html.erb
│ │ │ ├── _submissions.erb
│ │ │ └── _description.html.erb
│ │ ├── actions
│ │ │ ├── _edit.erb
│ │ │ ├── _unapprove.erb
│ │ │ ├── _destroy.erb
│ │ │ ├── _submission_instructions.erb
│ │ │ ├── _answer_questions.html.erb
│ │ │ ├── _review_submissions.html.erb
│ │ │ ├── _ask_question.html.erb
│ │ │ └── _submit.html.erb
│ │ ├── _description.html.erb
│ │ ├── _edit_attachment.erb
│ │ ├── _admin_links.erb
│ │ ├── submit.html.erb
│ │ ├── _vendor_actions.html.erb
│ │ ├── _timeline.html.erb
│ │ ├── _attachments.html.erb
│ │ ├── _pending_item.html.erb
│ │ ├── new.html.erb
│ │ ├── feed.xml.builder
│ │ ├── _subscribe_button.html.erb
│ │ ├── _view_attachment.html.erb
│ │ ├── _contact_info.html.erb
│ │ ├── show.html.erb
│ │ ├── index.html.erb
│ │ ├── pending.html.erb
│ │ ├── _questions.erb
│ │ ├── _admin_status.html.erb
│ │ ├── _search_results.html.erb
│ │ ├── edit.html.erb
│ │ └── _filter_form.html.erb
│ ├── submission_adapters
│ │ ├── email
│ │ │ └── _edit.html.erb
│ │ └── screendoor
│ │ │ ├── _edit.html.erb
│ │ │ └── _submit.html.erb
│ ├── home
│ │ ├── index.html.erb
│ │ ├── _hero.html.erb
│ │ ├── _about.html.erb
│ │ └── _recent_opportunities.html.erb
│ ├── static
│ │ └── about.html.erb
│ ├── layouts
│ │ ├── application.html.erb
│ │ └── base_mailer.html.erb
│ └── questions
│ │ └── _question.html.erb
├── helpers
│ ├── build_email_address_helper.rb
│ ├── pick_helper.rb
│ ├── formatting_helper.rb
│ ├── formatted_timestamp_helper.rb
│ ├── opportunities_helper.rb
│ └── application_helper.rb
├── inputs
│ └── datetime_picker_input.rb
├── uploaders
│ ├── base_uploader.rb
│ └── attachment_uploader.rb
├── filterers
│ └── opportunity_filterer.rb
├── policies
│ └── opportunity_policy.rb
└── dashboards
│ ├── saved_search_dashboard.rb
│ ├── category_dashboard.rb
│ ├── department_dashboard.rb
│ ├── attachment_dashboard.rb
│ └── question_dashboard.rb
├── lib
├── tasks
│ ├── .keep
│ ├── db.rake
│ └── auto_annotate_models.rake
├── submission_adapters
│ ├── none.rb
│ ├── screendoor.rb
│ ├── email.rb
│ └── base.rb
├── submission_adapters.rb
└── dispatch_configuration.rb
├── spec
├── models
│ ├── .keep
│ ├── category_spec.rb
│ ├── department_spec.rb
│ ├── saved_search_spec.rb
│ ├── question_spec.rb
│ ├── attachment_spec.rb
│ └── user_spec.rb
├── factories
│ ├── .keep
│ ├── saved_searches.rb
│ ├── departments.rb
│ ├── categories.rb
│ ├── attachments.rb
│ ├── questions.rb
│ └── users.rb
├── fixtures
│ └── files
│ │ ├── empty
│ │ ├── test.txt
│ │ └── test.docx
├── support
│ ├── webmock.rb
│ ├── database_cleaner.rb
│ └── utilities.rb
├── lib
│ └── dispatch_configuration_spec.rb
├── i18n_spec.rb
├── mailers
│ └── previews
│ │ ├── custom_devise_mailer_preview.rb
│ │ └── mailer_preview.rb
├── features
│ ├── sign_in_spec.rb
│ ├── home_spec.rb
│ ├── opportunities
│ │ ├── pending_spec.rb
│ │ ├── index_spec.rb
│ │ └── create_spec.rb
│ └── admin
│ │ └── base_spec.rb
├── spec_helper.rb
└── jobs
│ ├── user_search_results_job_spec.rb
│ └── send_deadline_reminders_job_spec.rb
├── .ruby-version
├── themes
└── dvl-core
│ ├── views
│ ├── .keep
│ └── application
│ │ └── _flashes.html.erb
│ ├── assets
│ ├── stylesheets
│ │ ├── theme
│ │ │ ├── layout
│ │ │ │ ├── .keep
│ │ │ │ └── application.scss
│ │ │ ├── components
│ │ │ │ ├── .keep
│ │ │ │ ├── signin_link.scss
│ │ │ │ ├── admin_actions.scss
│ │ │ │ ├── password_shim.scss
│ │ │ │ ├── simple_alert.scss
│ │ │ │ ├── opportunities_table.scss
│ │ │ │ ├── date_range.scss
│ │ │ │ ├── search_result_count.scss
│ │ │ │ ├── opportunity_actions.scss
│ │ │ │ ├── opportunity.scss
│ │ │ │ ├── filter_form.scss
│ │ │ │ ├── info_box.scss
│ │ │ │ ├── opportunity_timeline.scss
│ │ │ │ └── account_saved_searches.scss
│ │ │ ├── includes.scss
│ │ │ ├── overrides
│ │ │ │ ├── sidebar_box.scss
│ │ │ │ ├── navbar.scss
│ │ │ │ ├── hero.scss
│ │ │ │ ├── page_header.scss
│ │ │ │ └── rome.scss
│ │ │ ├── sections
│ │ │ │ ├── opportunities
│ │ │ │ │ ├── pending.scss
│ │ │ │ │ ├── show.scss
│ │ │ │ │ └── edit.scss
│ │ │ │ └── index.scss
│ │ │ ├── typography_overrides.scss
│ │ │ ├── branding.scss
│ │ │ └── dvl_core.scss
│ │ └── theme.css
│ └── javascripts
│ │ ├── initialize.coffee
│ │ ├── theme.js
│ │ └── selectize_monkeypatch.coffee
│ ├── locales
│ └── en.yml
│ ├── simple_form.rb
│ └── submission_adapters
│ └── dvl.rb
├── .tool-versions
├── vendor
└── assets
│ ├── javascripts
│ └── .keep
│ └── stylesheets
│ └── .keep
├── public
├── robots.txt
├── favicon.ico
├── apple-touch-icon-precomposed.png
├── non_digest_assets
│ └── README.txt
├── humans.txt
├── 500.html
├── 422.html
└── 404.html
├── Procfile
├── docs
├── screenshot.png
├── developing_dispatch_core.md
├── setting_up_a_development_environment.md
├── marketing.md
└── deployment.md
├── CHANGELOG.md
├── script
├── bootstrap
├── cibuild
└── server
├── config
├── environments
│ ├── staging.rb
│ ├── production.rb
│ ├── _smtp_env_vars.rb
│ ├── development.rb
│ ├── _shared.rb
│ ├── test.rb
│ └── _shared_staging_production.rb
├── initializers
│ ├── add_flash_types.rb
│ ├── filter_parameter_logging.rb
│ ├── mime_types.rb
│ ├── session_store.rb
│ ├── secret_token.rb
│ ├── wrap_parameters.rb
│ ├── carrierwave.rb
│ ├── inflections.rb
│ ├── theme.rb
│ └── whitelist_interceptor.rb
├── database.ci.yml
├── environment.rb
├── boot.rb
├── database.yml
├── schedule.rb
├── locales
│ └── simple_form.en.yml
├── application.rb
└── routes.rb
├── bin
├── bundle
├── rake
├── rspec
├── rails
├── guard
└── spring
├── config.ru
├── db
├── migrate
│ ├── 20160425151213_add_text_content_to_attachments.rb
│ └── 20160418203004_create_delayed_jobs.rb
└── seeds.rb
├── .editorconfig
├── Rakefile
├── circle.yml
├── .gitignore
├── Guardfile
├── .rubocop.yml
├── Gemfile
└── config.yml.example
/app/mailers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/models/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/tasks/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/models/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | 2.3.1
2 |
--------------------------------------------------------------------------------
/app/assets/images/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/factories/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/models/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/fixtures/files/empty:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/themes/dvl-core/views/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.tool-versions:
--------------------------------------------------------------------------------
1 | ruby 2.3.1
2 |
--------------------------------------------------------------------------------
/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vendor/assets/javascripts/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vendor/assets/stylesheets/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/javascripts/sections/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/fixtures/files/test.txt:
--------------------------------------------------------------------------------
1 | hi!
2 |
--------------------------------------------------------------------------------
/app/assets/javascripts/components/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/themes/dvl-core/assets/stylesheets/theme/layout/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/themes/dvl-core/locales/en.yml:
--------------------------------------------------------------------------------
1 | ---
2 | en:
3 | #
4 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # robotstxt.org/
2 |
3 | User-agent: *
--------------------------------------------------------------------------------
/themes/dvl-core/assets/stylesheets/theme/components/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/themes/dvl-core/simple_form.rb:
--------------------------------------------------------------------------------
1 | require 'dvl/simple_form_config'
2 |
--------------------------------------------------------------------------------
/app/jobs/application_job.rb:
--------------------------------------------------------------------------------
1 | class ApplicationJob < ActiveJob::Base
2 | end
3 |
--------------------------------------------------------------------------------
/app/views/users/registrations/edit.html.erb:
--------------------------------------------------------------------------------
1 | <%= render "edit_#{@edit_type}" %>
2 |
--------------------------------------------------------------------------------
/app/views/users/registrations/new.html.erb:
--------------------------------------------------------------------------------
1 | <%= render "new_#{@signup_type}" %>
2 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: bundle exec thin start -p $PORT
2 | worker: bundle exec rake jobs:work
3 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dobtco/dispatch/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/app/assets/javascripts/theme.js:
--------------------------------------------------------------------------------
1 | /* This will be overridden in the ./theme directory */
2 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/theme.css:
--------------------------------------------------------------------------------
1 | /* This will be overridden in the ./theme directory */
2 |
--------------------------------------------------------------------------------
/app/views/mailer/_greeting.html.erb:
--------------------------------------------------------------------------------
1 |
<%= t('mailer.greeting', name: @user.name) %>
2 |
--------------------------------------------------------------------------------
/docs/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dobtco/dispatch/HEAD/docs/screenshot.png
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | Change log
2 | ---
3 |
4 | (Will be populated once Dispatch is slightly more stable.)
5 |
--------------------------------------------------------------------------------
/app/assets/images/hero.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dobtco/dispatch/HEAD/app/assets/images/hero.jpg
--------------------------------------------------------------------------------
/app/assets/javascripts/components/selectize.coffee:
--------------------------------------------------------------------------------
1 | $ ->
2 | $('select[multiple]').selectize()
3 |
--------------------------------------------------------------------------------
/script/bootstrap:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 |
3 | bundle install
4 | rake db:setup
5 | rake db:seed:example
6 |
--------------------------------------------------------------------------------
/config/environments/staging.rb:
--------------------------------------------------------------------------------
1 | require_relative '_shared_staging_production'
2 | require_relative '_shared'
3 |
--------------------------------------------------------------------------------
/config/initializers/add_flash_types.rb:
--------------------------------------------------------------------------------
1 | ActionController::Base.send :add_flash_types, :error, :info, :success
2 |
--------------------------------------------------------------------------------
/spec/fixtures/files/test.docx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dobtco/dispatch/HEAD/spec/fixtures/files/test.docx
--------------------------------------------------------------------------------
/app/mailers/custom_devise_mailer.rb:
--------------------------------------------------------------------------------
1 | class CustomDeviseMailer < Devise::Mailer
2 | layout 'base_mailer'
3 | end
4 |
--------------------------------------------------------------------------------
/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | require_relative '_shared_staging_production'
2 | require_relative '_shared'
3 |
--------------------------------------------------------------------------------
/public/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dobtco/dispatch/HEAD/public/apple-touch-icon-precomposed.png
--------------------------------------------------------------------------------
/app/controllers/admin/users_controller.rb:
--------------------------------------------------------------------------------
1 | module Admin
2 | class UsersController < Admin::ApplicationController
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/app/controllers/static_controller.rb:
--------------------------------------------------------------------------------
1 | class StaticController < ApplicationController
2 | before_action :skip_authorization
3 | end
4 |
--------------------------------------------------------------------------------
/config/database.ci.yml:
--------------------------------------------------------------------------------
1 | test:
2 | host: localhost
3 | username: ubuntu
4 | database: circle_ruby_test
5 | adapter: postgresql
6 |
--------------------------------------------------------------------------------
/app/controllers/admin/questions_controller.rb:
--------------------------------------------------------------------------------
1 | module Admin
2 | class QuestionsController < Admin::ApplicationController
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/app/controllers/users/passwords_controller.rb:
--------------------------------------------------------------------------------
1 | module Users
2 | class PasswordsController < Devise::PasswordsController
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/app/controllers/users/sessions_controller.rb:
--------------------------------------------------------------------------------
1 | module Users
2 | class SessionsController < Devise::SessionsController
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/app/controllers/admin/attachments_controller.rb:
--------------------------------------------------------------------------------
1 | module Admin
2 | class AttachmentsController < Admin::ApplicationController
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/app/controllers/admin/categories_controller.rb:
--------------------------------------------------------------------------------
1 | module Admin
2 | class CategoriesController < Admin::ApplicationController
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/app/controllers/admin/departments_controller.rb:
--------------------------------------------------------------------------------
1 | module Admin
2 | class DepartmentsController < Admin::ApplicationController
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/script/cibuild:
--------------------------------------------------------------------------------
1 | #! /bin/sh
2 |
3 | set -e
4 | export RAILS_ENV=test
5 | bundle exec brakeman -z
6 | bundle exec rspec
7 | bundle exec rubocop
8 |
--------------------------------------------------------------------------------
/themes/dvl-core/assets/stylesheets/theme/includes.scss:
--------------------------------------------------------------------------------
1 | @import 'dvl/core/includes';
2 | @import 'branding';
3 | @import 'typography_overrides';
4 |
--------------------------------------------------------------------------------
/app/controllers/admin/opportunities_controller.rb:
--------------------------------------------------------------------------------
1 | module Admin
2 | class OpportunitiesController < Admin::ApplicationController
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/app/controllers/admin/saved_searches_controller.rb:
--------------------------------------------------------------------------------
1 | module Admin
2 | class SavedSearchesController < Admin::ApplicationController
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/bin/bundle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3 | load Gem.bin_path('bundler', 'bundle')
4 |
--------------------------------------------------------------------------------
/public/non_digest_assets/README.txt:
--------------------------------------------------------------------------------
1 | Wondering why this is here? Have fun: https://github.com/rails/sprockets-rails/issues/49#issuecomment-20535134
2 |
--------------------------------------------------------------------------------
/app/views/application/_flashes.html.erb:
--------------------------------------------------------------------------------
1 | <% flashes_with_consistent_keys.each do |k, v| %>
2 | <%= v %>
3 | <% end %>
4 |
--------------------------------------------------------------------------------
/app/views/users/registrations/_business_data.html.erb:
--------------------------------------------------------------------------------
1 | <% # f.input :"8a", as: :boolean, inline_label: 'Are you registered as an 8(a)?', label: false %>
2 |
--------------------------------------------------------------------------------
/app/assets/javascripts/components/data_toggle_visible.coffee:
--------------------------------------------------------------------------------
1 | $(document).on 'click', '[data-toggle-visible]', ->
2 | $($(@).data('toggle-visible')).toggle()
3 |
--------------------------------------------------------------------------------
/themes/dvl-core/assets/javascripts/initialize.coffee:
--------------------------------------------------------------------------------
1 | $ ->
2 | $('body').styledSelect()
3 | $('body').styledControls()
4 | $('body').formattedTimestamps()
5 |
--------------------------------------------------------------------------------
/lib/submission_adapters/none.rb:
--------------------------------------------------------------------------------
1 | module SubmissionAdapters
2 | class None < SubmissionAdapters::Base
3 | self.select_text = 'None of the above'
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/themes/dvl-core/assets/stylesheets/theme/overrides/sidebar_box.scss:
--------------------------------------------------------------------------------
1 | @import 'theme/includes';
2 |
3 | .sidebar_box + .sidebar_box {
4 | margin-top: $rhythm * 2;
5 | }
6 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require ::File.expand_path('../config/environment', __FILE__)
4 | run Rails.application
5 |
--------------------------------------------------------------------------------
/public/humans.txt:
--------------------------------------------------------------------------------
1 | # TEAM
2 | Department of Better Technology
3 | We design and build awesome software for government.
4 | http://www.dobt.co
5 | hello@dobt.co
6 | @dobtco
7 |
--------------------------------------------------------------------------------
/spec/support/webmock.rb:
--------------------------------------------------------------------------------
1 | require 'webmock/rspec'
2 |
3 | WebMock.disable_net_connect!(
4 | allow_localhost: true,
5 | allow: ['www.example.com', 'codeclimate.com']
6 | )
7 |
--------------------------------------------------------------------------------
/app/views/custom_devise_mailer/password_change.html.erb:
--------------------------------------------------------------------------------
1 | Hello <%= @resource.name %>!
2 |
3 | We're contacting you to notify you that your password has been changed.
4 |
--------------------------------------------------------------------------------
/app/assets/javascripts/sections/opportunities/edit.coffee:
--------------------------------------------------------------------------------
1 | $ ->
2 | return unless $('body').data('page-key') == 'opportunities-edit'
3 | $('form.edit_opportunity').progressGuard()
4 |
--------------------------------------------------------------------------------
/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require File.expand_path('../application', __FILE__)
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/themes/dvl-core/assets/stylesheets/theme/components/signin_link.scss:
--------------------------------------------------------------------------------
1 | @import 'theme/includes';
2 |
3 | .sign_in_link {
4 | margin-top: $lineHeight;
5 | font-size: $fontSmaller;
6 | }
7 |
--------------------------------------------------------------------------------
/themes/dvl-core/assets/stylesheets/theme/sections/opportunities/pending.scss:
--------------------------------------------------------------------------------
1 | @import 'theme/includes';
2 |
3 | .page_header_pending_opportunities {
4 | padding-top: $rhythm * 2;
5 | }
6 |
--------------------------------------------------------------------------------
/themes/dvl-core/assets/stylesheets/theme/sections/opportunities/show.scss:
--------------------------------------------------------------------------------
1 | @import 'theme/includes';
2 |
3 | .submission_instructions {
4 | margin-bottom: $rhythm; // consistency
5 | }
6 |
--------------------------------------------------------------------------------
/app/views/application/_inner_layout.html.erb:
--------------------------------------------------------------------------------
1 | <%= yield(:before_container) %>
2 |
3 |
4 | <%= yield %>
5 |
6 |
--------------------------------------------------------------------------------
/app/assets/javascripts/sections/opportunities/index.coffee:
--------------------------------------------------------------------------------
1 | $(document).on 'click', '.js-toggle-filters', ->
2 | $(@).toggleClass('is_active')
3 | $('.info_box_filters').toggleClass('is_active')
4 |
--------------------------------------------------------------------------------
/app/views/opportunities/edit/_contact.html.erb:
--------------------------------------------------------------------------------
1 | <%= f.input :contact_name, label: t('name') %>
2 | <%= f.input :contact_email, label: t('email') %>
3 | <%= f.input :contact_phone, label: t('phone') %>
4 |
--------------------------------------------------------------------------------
/config/boot.rb:
--------------------------------------------------------------------------------
1 | # Set up gems listed in the Gemfile.
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3 |
4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
5 |
--------------------------------------------------------------------------------
/themes/dvl-core/assets/stylesheets/theme/components/admin_actions.scss:
--------------------------------------------------------------------------------
1 | @import 'theme/includes';
2 |
3 | .admin_actions {
4 | margin-top: $rhythm;
5 | li {
6 | text-align: right;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/app/views/opportunities/actions/_edit.erb:
--------------------------------------------------------------------------------
1 | <% if policy(@opportunity).edit? && @opportunity.approved? %>
2 |
3 | <%= t('edit') %>
4 |
5 | <% end %>
6 |
--------------------------------------------------------------------------------
/db/migrate/20160425151213_add_text_content_to_attachments.rb:
--------------------------------------------------------------------------------
1 | class AddTextContentToAttachments < ActiveRecord::Migration
2 | def change
3 | add_column :attachments, :text_content, :text
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/script/server:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -e
4 |
5 | cd $(dirname "$0")/..
6 | : ${RAILS_ENV:=development}
7 | : ${RACK_ENV:=development}
8 |
9 | export RAILS_ENV RACK_ENV
10 |
11 | bundle exec thin start
12 |
--------------------------------------------------------------------------------
/app/jobs/queue_user_search_results_job.rb:
--------------------------------------------------------------------------------
1 | class QueueUserSearchResultsJob < ApplicationJob
2 | def perform
3 | User.find_each do |user|
4 | UserSearchResultsJob.perform_later(user)
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/app/views/submission_adapters/email/_edit.html.erb:
--------------------------------------------------------------------------------
1 | <%= f.input :submit_to do %>
2 | <%= f.input :name, required: true, wrapper: :vertical %>
3 | <%= f.input :email, required: true, wrapper: :vertical %>
4 | <% end %>
5 |
--------------------------------------------------------------------------------
/app/views/application/_webfonts.html.erb:
--------------------------------------------------------------------------------
1 | <% unless Rails.env.test? %>
2 |
3 | <% end %>
4 |
--------------------------------------------------------------------------------
/themes/dvl-core/assets/stylesheets/theme.css:
--------------------------------------------------------------------------------
1 | /*
2 | *= require theme/dvl_core
3 | *= require_tree ./theme/layout
4 | *= require_tree ./theme/components
5 | *= require_tree ./theme/overrides
6 | *= require_tree ./theme/sections
7 | */
8 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | trim_trailing_whitespace = true
6 | insert_final_newline = true
7 | indent_style = spaces
8 | indent_size = 2
9 |
10 | [*.md]
11 | trim_trailing_whitespace = true
12 |
--------------------------------------------------------------------------------
/app/views/submission_adapters/screendoor/_edit.html.erb:
--------------------------------------------------------------------------------
1 | <%= f.input :embed_token, required: true do %>
2 | <%= f.input_field :embed_token %>
3 | <%= t('embed_token_hint') %>
4 | <% end %>
5 |
--------------------------------------------------------------------------------
/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | begin
3 | load File.expand_path('../spring', __FILE__)
4 | rescue LoadError => e
5 | raise unless e.message.include?('spring')
6 | end
7 | require 'bundler/setup'
8 | load Gem.bin_path('rake', 'rake')
9 |
--------------------------------------------------------------------------------
/spec/lib/dispatch_configuration_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe DispatchConfiguration do
4 | it 'loads the defaults in config.yml.example' do
5 | expect(described_class.upload_storage).to eq 'file'
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/app/assets/javascripts/base.coffee:
--------------------------------------------------------------------------------
1 | # Allow us to use href='#' without jumping to the top of the page.
2 | # Otherwise, we'd have to use javascript:void(0) which sucks.
3 | $(document).on 'click', '[href="#"]', (e) ->
4 | e.preventDefault()
5 |
--------------------------------------------------------------------------------
/app/views/opportunities/actions/_unapprove.erb:
--------------------------------------------------------------------------------
1 | <% if policy(@opportunity).approve? && @opportunity.approved? %>
2 |
3 | <%= t('unapprove') %>
4 |
5 | <% end %>
6 |
--------------------------------------------------------------------------------
/bin/rspec:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | begin
3 | load File.expand_path('../spring', __FILE__)
4 | rescue LoadError => e
5 | raise unless e.message.include?('spring')
6 | end
7 | require 'bundler/setup'
8 | load Gem.bin_path('rspec-core', 'rspec')
9 |
--------------------------------------------------------------------------------
/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Configure sensitive parameters which will be filtered from the log file.
4 | Rails.application.config.filter_parameters += [:password]
5 |
--------------------------------------------------------------------------------
/config/initializers/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 | # Mime::Type.register_alias "text/html", :iphone
6 |
--------------------------------------------------------------------------------
/themes/dvl-core/assets/javascripts/theme.js:
--------------------------------------------------------------------------------
1 | //= require_self
2 | //= require dvl/core/styled_select
3 | //= require dvl/core/styled_controls
4 | //= require dvl/components/navbar
5 | //= require dvl/components/flashes
6 | //= require_tree .
7 |
8 | var Dvl = {};
9 |
--------------------------------------------------------------------------------
/themes/dvl-core/submission_adapters/dvl.rb:
--------------------------------------------------------------------------------
1 | # class DvlSubmission < SubmissionAdapters::Base
2 | # self.name = 'Dvl'
3 | #
4 | # def submit_proposals_instructions
5 | # 'I dunno!'
6 | # end
7 | # end
8 | #
9 | # SubmissionAdapters.all_adapters << DvlSubmission
10 |
--------------------------------------------------------------------------------
/themes/dvl-core/views/application/_flashes.html.erb:
--------------------------------------------------------------------------------
1 | <% flashes_with_consistent_keys.each do |k, (text, links)| %>
2 |
7 | <% end %>
8 |
--------------------------------------------------------------------------------
/app/views/opportunities/actions/_destroy.erb:
--------------------------------------------------------------------------------
1 | <% if policy(@opportunity).destroy? %>
2 |
3 | '>
4 | <%= t('destroy') %>
5 |
6 |
7 | <% end %>
8 |
--------------------------------------------------------------------------------
/app/views/users/registrations/confirm.erb:
--------------------------------------------------------------------------------
1 | <% content_for(:page_title, t('.title')) %>
2 | <% content_for(:main_container_class, 'container_tiny') %>
3 |
4 |
7 |
8 | <%= t('.body') %>
9 |
--------------------------------------------------------------------------------
/app/views/opportunities/_description.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <% if @opportunity.description.present? %>
3 | <%= format_textarea_input(@opportunity.description) %>
4 | <% else %>
5 |
<%= t('no_description') %>
6 | <% end %>
7 |
8 |
--------------------------------------------------------------------------------
/app/assets/javascripts/components/data_show_if_checked.coffee:
--------------------------------------------------------------------------------
1 | $ ->
2 | $('[data-show-if-checked]').each ->
3 | $input = $("input[name=\"#{$(@).data('show-if-checked')}\"][type=checkbox]")
4 |
5 | $input.on 'click', =>
6 | if $input.is(':checked') then $(@).show() else $(@).hide()
7 |
--------------------------------------------------------------------------------
/lib/submission_adapters.rb:
--------------------------------------------------------------------------------
1 | module SubmissionAdapters
2 | class << self
3 | mattr_accessor :all_adapters do
4 | [
5 | SubmissionAdapters::Email,
6 | SubmissionAdapters::Screendoor,
7 | SubmissionAdapters::None
8 | ]
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/views/opportunities/actions/_submission_instructions.erb:
--------------------------------------------------------------------------------
1 | <% if policy(@opportunity).submit? && @opportunity.submission_adapter.submit_proposals_instructions %>
2 | <%= @opportunity.submission_adapter.submit_proposals_instructions %>
3 | <% end %>
4 |
--------------------------------------------------------------------------------
/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,
4 | key: '_dispatch_session',
5 | secure: DispatchConfiguration.ssl
6 |
--------------------------------------------------------------------------------
/app/views/opportunities/_edit_attachment.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= attachment.upload.raw_filename %>
3 | × <%= t('destroy') %>
4 |
5 |
--------------------------------------------------------------------------------
/app/assets/javascripts/components/data_toggle_text.coffee:
--------------------------------------------------------------------------------
1 | $.fn.extend
2 | toggleText: ->
3 | @each ->
4 | newText = $(@).data('toggle-text')
5 | $(@).data 'toggle-text', $(@).text()
6 | $(@).text newText
7 |
8 | $(document).on 'click', '[data-toggle-text]', ->
9 | $(@).toggleText()
10 |
--------------------------------------------------------------------------------
/app/helpers/build_email_address_helper.rb:
--------------------------------------------------------------------------------
1 | module BuildEmailAddressHelper
2 | def build_email_address(email, name)
3 | address = Mail::Address.new(email)
4 | address.display_name = name.dup if name.present?
5 | address.format
6 | rescue Mail::Field::ParseError
7 | email
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | begin
3 | load File.expand_path('../spring', __FILE__)
4 | rescue LoadError => e
5 | raise unless e.message.include?('spring')
6 | end
7 | APP_PATH = File.expand_path('../../config/application', __FILE__)
8 | require_relative '../config/boot'
9 | require 'rails/commands'
10 |
--------------------------------------------------------------------------------
/themes/dvl-core/assets/stylesheets/theme/overrides/navbar.scss:
--------------------------------------------------------------------------------
1 | @import 'theme/includes';
2 |
3 | .navbar_brand {
4 | font-family: $fontFamilyDisplay;
5 | font-size: 2rem;
6 | }
7 |
8 | @media screen and (min-width: $deskWidth) {
9 | .navbar .button {
10 | margin-left: $rhythm * 2;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/app/views/opportunities/actions/_answer_questions.html.erb:
--------------------------------------------------------------------------------
1 | <% if policy(@opportunity).answer_questions? && @opportunity.questions.unanswered.count > 0 %>
2 |
3 |
4 | <%= t('unanswered_questions', count: @opportunity.questions.unanswered.count) %>
5 |
6 |
7 | <% end %>
8 |
--------------------------------------------------------------------------------
/app/views/mailer/search_results.html.erb:
--------------------------------------------------------------------------------
1 | <%= render('greeting') %>
2 |
3 | <%= t('mailer.search_results.body') %>
4 |
5 |
12 |
--------------------------------------------------------------------------------
/config/database.yml:
--------------------------------------------------------------------------------
1 | development:
2 | adapter: postgresql
3 | database: dispatch_development
4 | port: <%= ENV["BOXEN_POSTGRESQL_PORT"] || 5432 %>
5 | host: localhost
6 |
7 | test:
8 | adapter: postgresql
9 | database: dispatch_test
10 | port: <%= ENV["BOXEN_POSTGRESQL_PORT"] || 5432 %>
11 | host: localhost
12 |
--------------------------------------------------------------------------------
/themes/dvl-core/assets/stylesheets/theme/components/password_shim.scss:
--------------------------------------------------------------------------------
1 | @import 'theme/includes';
2 |
3 | .password_shim {
4 | line-height: $rhythm * 4;
5 | letter-spacing: 0.2rem;
6 | position: relative;
7 | top: $rhythm / -2;
8 | font-size: $lineHeight;
9 | color: $darkerGray;
10 | margin-right: $rhythm / 2;
11 | }
12 |
--------------------------------------------------------------------------------
/app/controllers/home_controller.rb:
--------------------------------------------------------------------------------
1 | class HomeController < ApplicationController
2 | def index
3 | skip_authorization
4 |
5 | @recent_opportunities = Opportunity.
6 | posted.
7 | order_by_recently_posted.
8 | limit(5)
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/app/views/home/index.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for(:page_title, t('home.hero.title')) %>
2 | <% content_for(:page_key, 'index') %>
3 |
4 | <% content_for(:before_container) do %>
5 | <%= render('hero') %>
6 | <% end %>
7 |
8 |
9 | <%= render('recent_opportunities') %>
10 | <%= render('about') %>
11 |
12 |
--------------------------------------------------------------------------------
/themes/dvl-core/assets/stylesheets/theme/components/simple_alert.scss:
--------------------------------------------------------------------------------
1 | @import 'theme/includes';
2 |
3 | .simple_alert {
4 | background: $lightestGray;
5 | text-align: center;
6 | display: block;
7 | color: $darkestGray;
8 | @include font_smoothing;
9 | padding: $rhythm ($rhythm * 1.5);
10 | border-radius: $radius;
11 | }
12 |
--------------------------------------------------------------------------------
/themes/dvl-core/assets/stylesheets/theme/typography_overrides.scss:
--------------------------------------------------------------------------------
1 | $fontLineHeightH1: $rhythm * 8;
2 | $fontLineHeightH2: $rhythm * 5;
3 |
4 | $weightBold: 700;
5 |
6 | h3 {
7 | @include fontDisplay;
8 | }
9 |
10 | @media screen and (max-width: $lapWidth) {
11 | h2, h3 {
12 | font-family: $fontFamilyDisplay;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/app/views/custom_devise_mailer/confirmation_instructions.html.erb:
--------------------------------------------------------------------------------
1 | Welcome, <%= @resource.name %>!
2 |
3 | To finish signing up, just confirm your email address by pressing the button below:
4 |
5 | Confirm my account
6 |
--------------------------------------------------------------------------------
/app/views/opportunities/_admin_links.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= render('opportunities/actions/review_submissions') %>
3 | <%= render('opportunities/actions/answer_questions') %>
4 | <%= render('opportunities/actions/edit') %>
5 | <%= render('opportunities/actions/destroy') %>
6 | <%= render('opportunities/actions/unapprove') %>
7 |
8 |
--------------------------------------------------------------------------------
/app/views/opportunities/submit.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for(:page_title, t('submit_proposal')) %>
2 | <% content_for(:main_container_class, 'container_small') %>
3 |
4 |
7 |
8 | <%= render "submission_adapters/#{@opportunity.submission_adapter.to_param}/submit" %>
9 |
--------------------------------------------------------------------------------
/themes/dvl-core/assets/stylesheets/theme/components/opportunities_table.scss:
--------------------------------------------------------------------------------
1 | @import 'theme/includes';
2 |
3 | .opportunities_table_item {
4 | tr:first-child {
5 | td {
6 | border-bottom: 0;
7 | padding-bottom: 0;
8 | }
9 | }
10 |
11 | tr:last-child {
12 | font-size: $fontSmall;
13 | color: $darkerGray;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/controllers/users/confirmations_controller.rb:
--------------------------------------------------------------------------------
1 | module Users
2 | class ConfirmationsController < Devise::ConfirmationsController
3 | # Automatically sign in after confirming email address.
4 | def show
5 | super do
6 | if resource.errors.empty?
7 | sign_in(resource)
8 | end
9 | end
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # This file should contain all the record creation needed to seed the database with its default values.
2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
3 | #
4 | # Examples:
5 | #
6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }])
7 | # Mayor.create(name: 'Emanuel', city: cities.first)
8 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require File.expand_path('../config/application', __FILE__)
2 |
3 | Rails.application.load_tasks
4 |
5 | # Don't dump database structure in deployed environments
6 | Rake::Task['db:structure:dump'].clear if Rails.env.in? %w(production staging)
7 |
8 | # Log to console in development
9 | if Rails.env.development?
10 | Rails.logger = Logger.new(STDOUT)
11 | end
12 |
--------------------------------------------------------------------------------
/app/assets/javascripts/components/data_show_if_selected.coffee:
--------------------------------------------------------------------------------
1 | $ ->
2 | $('[data-show-if-selected]').each ->
3 | $el = $(@)
4 | [selector, valueToMatch] = $(@).data('show-if-selected').split('|')
5 | $input = $(selector)
6 | showHide = -> $el[if $input.val() in valueToMatch.split(',') then 'show' else 'hide']()
7 | $input.on 'change.show_if_selected', $input, showHide
8 |
--------------------------------------------------------------------------------
/app/views/mailer/question_deadline.html.erb:
--------------------------------------------------------------------------------
1 | <%= render('greeting') %>
2 |
3 |
4 | <%= t('mailer.question_deadline.body_html', href: opportunity_url(@opportunity), title: @opportunity.title) %>
5 |
6 |
7 |
8 |
9 | <%= t('ask_question') %> →
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/views/mailer/submission_deadline.html.erb:
--------------------------------------------------------------------------------
1 | <%= render('greeting') %>
2 |
3 |
4 | <%= t('mailer.submission_deadline.body_html', href: opportunity_url(@opportunity), title: @opportunity.title) %>
5 |
6 |
7 |
8 |
9 | <%= t('submit_proposal') %> →
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/models/category.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: categories
4 | #
5 | # id :integer not null, primary key
6 | # name :string
7 | # created_at :datetime not null
8 | # updated_at :datetime not null
9 | #
10 |
11 | class Category < ActiveRecord::Base
12 | has_and_belongs_to_many :opportunities
13 | has_and_belongs_to_many :users
14 | end
15 |
--------------------------------------------------------------------------------
/app/views/mailer/approval_request.html.erb:
--------------------------------------------------------------------------------
1 | <%= render('greeting') %>
2 |
3 |
4 | <%= t('mailer.approval_request.body_html', creator: @creator.name, href: opportunity_url(@opportunity), title: @opportunity.title) %>
5 |
6 |
7 |
8 |
9 | <%= t('view_opportunity') %> →
10 |
11 |
12 |
--------------------------------------------------------------------------------
/spec/models/category_spec.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: categories
4 | #
5 | # id :integer not null, primary key
6 | # name :string
7 | # created_at :datetime not null
8 | # updated_at :datetime not null
9 | #
10 |
11 | require 'spec_helper'
12 |
13 | describe Category do
14 | subject { build(:category) }
15 | it { should be_valid }
16 | end
17 |
--------------------------------------------------------------------------------
/app/inputs/datetime_picker_input.rb:
--------------------------------------------------------------------------------
1 | class DatetimePickerInput < SimpleForm::Inputs::StringInput
2 | def initialize(*)
3 | super
4 |
5 | # Format value as iso8601 for javascript new Date() compatibility
6 | if object && (value = object.send(attribute_name))
7 | input_html_options[:value] = value.iso8601
8 | end
9 |
10 | input_html_options[:placeholder] = 'MM/DD/YYYY'
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/themes/dvl-core/assets/stylesheets/theme/components/date_range.scss:
--------------------------------------------------------------------------------
1 | @import 'theme/includes';
2 |
3 | .date_range {
4 | input {
5 | display: inline;
6 | width: 11rem;
7 | }
8 |
9 | span {
10 | font-size: $fontSmaller;
11 | color: $darkerGray;
12 | }
13 |
14 | input, span {
15 | margin-right: $rhythm;
16 |
17 | &:last-child {
18 | margin-right: 0;
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/themes/dvl-core/assets/stylesheets/theme/overrides/hero.scss:
--------------------------------------------------------------------------------
1 | @import 'theme/includes';
2 |
3 | .hero {
4 | background: $primaryColor url(asset-path('hero.jpg')) no-repeat center;
5 | background-size: cover;
6 | text-align: center;
7 | padding-top: $lineHeight * 5;
8 | h1 {
9 | margin: 0 0 ($rhythm * 2);
10 | }
11 | p {
12 | margin: 0 auto $lineHeight;
13 | max-width: 30rem;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/spec/models/department_spec.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: departments
4 | #
5 | # id :integer not null, primary key
6 | # name :string
7 | # created_at :datetime not null
8 | # updated_at :datetime not null
9 | #
10 |
11 | require 'spec_helper'
12 |
13 | describe Department do
14 | subject { build(:department) }
15 | it { should be_valid }
16 | end
17 |
--------------------------------------------------------------------------------
/app/views/opportunities/_vendor_actions.html.erb:
--------------------------------------------------------------------------------
1 | <% if policy(@opportunity).submit? || policy(@opportunity).ask_question? %>
2 |
3 | <%= render('opportunities/actions/submit') %>
4 | <%= render('opportunities/actions/ask_question') %>
5 |
6 |
7 | <%= render('opportunities/actions/submission_instructions') %>
8 | <% end %>
9 |
10 | <%= render('opportunities/contact_info') %>
11 |
--------------------------------------------------------------------------------
/themes/dvl-core/assets/javascripts/selectize_monkeypatch.coffee:
--------------------------------------------------------------------------------
1 | # Monkeypatch Selectize to not use "item" class, which conlflits with dvl-core...
2 | oldSetupTemplates = Selectize::setupTemplates
3 |
4 | Selectize::setupTemplates = ->
5 | oldSetupTemplates.apply(@, arguments)
6 | @settings.render.item = (data, escape) ->
7 | """
8 | #{escape(data[@settings.labelField])}
9 | """
10 |
--------------------------------------------------------------------------------
/app/models/department.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: departments
4 | #
5 | # id :integer not null, primary key
6 | # name :string
7 | # created_at :datetime not null
8 | # updated_at :datetime not null
9 | #
10 |
11 | class Department < ActiveRecord::Base
12 | has_many :opportunities, dependent: :nullify
13 |
14 | default_scope -> { order('LOWER(name)') }
15 | end
16 |
--------------------------------------------------------------------------------
/app/views/opportunities/actions/_review_submissions.html.erb:
--------------------------------------------------------------------------------
1 | <% if policy(@opportunity).review_submissions? %>
2 |
3 | <% if @opportunity.submission_adapter.view_proposals_url %>
4 |
5 | <%= @opportunity.submission_adapter.view_proposals_link_text.presence || t('view_proposals') %>
6 |
7 | <% end %>
8 |
9 | <% end %>
10 |
--------------------------------------------------------------------------------
/spec/i18n_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require 'i18n/tasks'
3 |
4 | RSpec.describe 'I18n' do
5 | let(:i18n) { I18n::Tasks::BaseTask.new }
6 | let(:missing_keys) { i18n.missing_keys }
7 | let(:unused_keys) { i18n.unused_keys }
8 |
9 | it 'does not have missing keys' do
10 | expect(missing_keys).to be_empty
11 | end
12 |
13 | it 'does not have unused keys' do
14 | expect(unused_keys).to be_empty
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/themes/dvl-core/assets/stylesheets/theme/components/search_result_count.scss:
--------------------------------------------------------------------------------
1 | @import 'theme/includes';
2 |
3 | .search_result_count {
4 | display: block;
5 | background: $lightestGray;
6 | font-size: $fontSmaller;
7 | white-space: nowrap;
8 | padding: $rhythm ($rhythm * 2);
9 | border-radius: $radius;
10 | position: relative;
11 | top: $rhythm / -2;
12 | @media screen and (max-width: $lapWidth - 1) {
13 | display: none;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/views/opportunities/_timeline.html.erb:
--------------------------------------------------------------------------------
1 | <% if opportunity_timeline_events.present? %>
2 |
10 | <% end %>
11 |
--------------------------------------------------------------------------------
/bin/guard:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | #
3 | # This file was generated by Bundler.
4 | #
5 | # The application 'guard' is installed as part of a gem, and
6 | # this file is here to facilitate running it.
7 | #
8 |
9 | require 'pathname'
10 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
11 | Pathname.new(__FILE__).realpath)
12 |
13 | require 'rubygems'
14 | require 'bundler/setup'
15 |
16 | load Gem.bin_path('guard', 'guard')
17 |
--------------------------------------------------------------------------------
/spec/mailers/previews/custom_devise_mailer_preview.rb:
--------------------------------------------------------------------------------
1 | class CustomDeviseMailerPreview < ActionMailer::Preview
2 | def confirmation_instructions
3 | CustomDeviseMailer.confirmation_instructions(
4 | User.first || FactoryGirl.create(:user),
5 | 'xxx'
6 | )
7 | end
8 |
9 | def reset_password_instructions
10 | CustomDeviseMailer.reset_password_instructions(
11 | User.first || FactoryGirl.create(:user),
12 | 'xxx'
13 | )
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/spec/factories/saved_searches.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: saved_searches
4 | #
5 | # id :integer not null, primary key
6 | # user_id :integer
7 | # search_params :text
8 | # created_at :datetime not null
9 | # updated_at :datetime not null
10 | #
11 | # Indexes
12 | #
13 | # index_saved_searches_on_user_id (user_id)
14 | #
15 |
16 | FactoryGirl.define do
17 | factory :saved_search do
18 | user
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/app/views/home/_hero.html.erb:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/app/views/opportunities/actions/_ask_question.html.erb:
--------------------------------------------------------------------------------
1 | <% if policy(@opportunity).ask_question? %>
2 |
3 | <%= t('ask_question') %>
4 |
5 | <% elsif OpportunityPolicy.new(User.new, @opportunity).ask_question? %>
6 |
7 | <%= t('ask_question') %>
8 |
9 | <% end %>
10 |
--------------------------------------------------------------------------------
/app/views/mailer/question_answered.html.erb:
--------------------------------------------------------------------------------
1 | <%= render('greeting') %>
2 |
3 |
4 | <%= t('mailer.question_answered.body_html', href: opportunity_url(@question.opportunity), title: @question.opportunity.title) %>
5 |
6 |
7 |
8 | <%= format_textarea_input(@question.answer_text) %>
9 |
10 |
11 |
12 | <%= t('view_opportunity') %> →
13 |
14 |
--------------------------------------------------------------------------------
/spec/support/database_cleaner.rb:
--------------------------------------------------------------------------------
1 | RSpec.configure do |config|
2 | config.before(:suite) do
3 | DatabaseCleaner.clean_with(:truncation)
4 | end
5 |
6 | config.before(:each) do
7 | DatabaseCleaner.strategy = :transaction
8 | end
9 |
10 | config.before(:each, js: true) do
11 | DatabaseCleaner.strategy = :truncation
12 | end
13 |
14 | config.before(:each) do
15 | DatabaseCleaner.start
16 | end
17 |
18 | config.after(:each) do
19 | DatabaseCleaner.clean
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/app/views/custom_devise_mailer/reset_password_instructions.html.erb:
--------------------------------------------------------------------------------
1 | Hello <%= @resource.name %>!
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), class: 'btn-primary' %>
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/opportunities/_attachments.html.erb:
--------------------------------------------------------------------------------
1 | <% if @opportunity.attachments.present? %>
2 |
3 |
6 |
7 |
8 | <% @opportunity.attachments.each do |attachment| %>
9 | <%= render partial: 'view_attachment', locals: { attachment: attachment } %>
10 | <% end %>
11 |
12 |
13 | <% end %>
14 |
--------------------------------------------------------------------------------
/themes/dvl-core/assets/stylesheets/theme/branding.scss:
--------------------------------------------------------------------------------
1 | $navBackground: #2E2334;
2 |
3 | $primaryColor: #2E2334;
4 | $secondaryColor: #ED8442;
5 |
6 | $fontFamilyDisplay: 'Passion One', 'Helvetica Neue', Arial, sans-serif;
7 | $fontFamilyDefault: 'Roboto', 'HelveticaNeue-Light', Helvetica, Arial, sans-serif;
8 | $fontFamilyMonospace: 'Monaco', 'Lucida Console', monospace;
9 |
10 | // Smooth text looks best against our $primaryColor
11 |
12 | .navbar a,
13 | .button.primary {
14 | @include font_smoothing;
15 | }
16 |
--------------------------------------------------------------------------------
/app/assets/javascripts/components/submission_adapters.coffee:
--------------------------------------------------------------------------------
1 | disableInvisibleInputs = ->
2 | $('[data-adapter-name]').each ->
3 | if $(@).is(':visible')
4 | $(@).find(':input').prop('disabled', false)
5 | else
6 | $(@).find(':input').prop('disabled', true)
7 |
8 | $ ->
9 | $('#opportunity_submission_adapter_name').on 'change', ->
10 | $('[data-adapter-name]').hide()
11 | $("[data-adapter-name=\"#{$(@).val()}\"]").show()
12 | disableInvisibleInputs()
13 |
14 | disableInvisibleInputs()
15 |
--------------------------------------------------------------------------------
/app/helpers/pick_helper.rb:
--------------------------------------------------------------------------------
1 | module PickHelper
2 | def pick(obj, *keys)
3 | stringified_keys = keys.map(&:to_s)
4 |
5 | {}.tap do |h|
6 | if obj.is_a?(Hash)
7 | obj.each do |key, value|
8 | h[key.to_sym] = value if stringified_keys.include?(key.to_s)
9 | end
10 | else
11 | stringified_keys.each do |key|
12 | if obj.respond_to?(key)
13 | h[key.to_sym] = obj.send(key)
14 | end
15 | end
16 | end
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/app/views/mailer/question_asked.html.erb:
--------------------------------------------------------------------------------
1 | <%= render('greeting') %>
2 |
3 |
4 | <%= t('mailer.question_asked.body_html', href: opportunity_url(@question.opportunity), title: @question.opportunity.title) %>
5 |
6 |
7 |
8 | <%= format_textarea_input(@question.question_text) %>
9 |
10 |
11 |
12 | '><%= t('answer_question') %> →
13 |
14 |
--------------------------------------------------------------------------------
/app/views/users/passwords/new.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for(:page_title, 'Forgot your password?') %>
2 | <% content_for(:main_container_class, 'container_tiny') %>
3 |
4 |
7 |
8 | <%= simple_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %>
9 | <%= f.input :email, required: true, autofocus: true %>
10 | <%= f.button :submit, "Send me reset password instructions", class: 'primary' %>
11 | <% end %>
12 |
--------------------------------------------------------------------------------
/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 | if (match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m))
11 | Gem.paths = { 'GEM_PATH' => [Bundler.bundle_path.to_s, *Gem.path].uniq }
12 | gem 'spring', match[1]
13 | require 'spring/binstub'
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/config/initializers/secret_token.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 | # Make sure the secret is at least 30 characters and all random,
6 | # no regular words or you'll be exposed to dictionary attacks.
7 |
8 | Rails.application.config.secret_token = DispatchConfiguration.secret_token
9 | Rails.application.config.secret_key_base = DispatchConfiguration.secret_key_base
10 |
--------------------------------------------------------------------------------
/spec/models/saved_search_spec.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: saved_searches
4 | #
5 | # id :integer not null, primary key
6 | # user_id :integer
7 | # search_params :text
8 | # created_at :datetime not null
9 | # updated_at :datetime not null
10 | #
11 | # Indexes
12 | #
13 | # index_saved_searches_on_user_id (user_id)
14 | #
15 |
16 | require 'spec_helper'
17 |
18 | describe SavedSearch do
19 | subject { build(:saved_search) }
20 | it { should be_valid }
21 | end
22 |
--------------------------------------------------------------------------------
/app/views/opportunities/_pending_item.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= opportunity.title %>
4 |
5 | <% if opportunity.approved? %>
6 | <%= t('will_publish_at_time', timestamp: long_timestamp(opportunity.publish_at)).html_safe %>
7 | <% else %>
8 | <%= t('created_by_user_at_time', user: opportunity.created_by_user.name, timestamp: long_timestamp(opportunity.created_at)).html_safe %>
9 | <% end %>
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/spec/features/sign_in_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'Signing in' do
4 | let!(:user) { create(:user, password: 'password') }
5 |
6 | it 'works properly' do
7 | visit root_path
8 | click_link t('sign_in')
9 | fill_in 'Email address', with: user.email
10 | fill_in 'Password', with: 'password'
11 | click_button t('sign_in')
12 | expect(page.body).to include t('devise.sessions.signed_in')
13 | click_link t('sign_out')
14 | expect(page.body).to include t('devise.sessions.signed_out')
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/app/views/opportunities/new.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for(:page_key, 'opportunities-new') %>
2 | <% content_for(:page_title, t('post_an_opportunity')) %>
3 | <% content_for(:main_container_class, 'container_tiny') %>
4 |
5 |
8 |
9 | <%= simple_form_for @opportunity do |f| %>
10 | <%= f.input :title, label: false, input_html: { class: 'large', 'aria-label' => t('.whats_the_title') } %>
11 | <%= f.button :button, t('get_started'), class: 'primary' %>
12 | <% end %>
13 |
--------------------------------------------------------------------------------
/app/helpers/formatting_helper.rb:
--------------------------------------------------------------------------------
1 | module FormattingHelper
2 | def format_textarea_input(x)
3 | Rinku.auto_link(simpler_format(x), :all, "rel='nofollow'").html_safe
4 | end
5 |
6 | def simpler_format(text)
7 | text = '' if text.nil?
8 | text = text.dup
9 | text = CGI.escapeHTML(text)
10 | text = text.to_str
11 | text.gsub!(/\r\n?/, "\n") # \r\n and \r -> \n
12 | text.gsub!(/\n\n+/, ' ') # 2+ newline -> br br
13 | text.gsub!(/([^\n]\n)(?=[^\n])/, '\1 ') # 1 newline -> br
14 | text.html_safe
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/app/views/opportunities/actions/_submit.html.erb:
--------------------------------------------------------------------------------
1 | <% if policy(@opportunity).submit? %>
2 | <% if @opportunity.submission_adapter.submission_page %>
3 |
4 | <%= t('submit_proposal') %>
5 |
6 | <% elsif @opportunity.submission_adapter.submit_proposals_url %>
7 |
8 | <%= t('submit_proposal') %>
9 |
10 | <% end %>
11 | <% end %>
12 |
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | experimental:
2 | notify:
3 | branches:
4 | only:
5 | - master
6 |
7 | machine:
8 | environment:
9 | GEMNASIUM_TESTSUITE: 'script/cibuild'
10 | GEMNASIUM_PROJECT_SLUG: 'github.com/dobtco/dispatch'
11 |
12 | dependencies:
13 | cache_directories:
14 | - "public/assets"
15 | - "tmp/cache/assets"
16 |
17 | database:
18 | override:
19 | - mv config/database.ci.yml config/database.yml
20 | - bundle exec rake db:setup
21 | post:
22 | - "RAILS_ENV=test bundle exec rake assets:precompile assets:clean[0]"
23 |
--------------------------------------------------------------------------------
/themes/dvl-core/assets/stylesheets/theme/components/opportunity_actions.scss:
--------------------------------------------------------------------------------
1 | @import 'theme/includes';
2 |
3 | .opportunity_actions {
4 | margin-left: - $rhythm;
5 | font-size: 0;
6 | li {
7 | font-size: 100%;
8 | display: inline-block;
9 | padding-left: $rhythm;
10 | position: relative;
11 | width: 50%;
12 | @media screen and (min-width: $lapWidth) {
13 | width: 100%;
14 | }
15 | @media screen and (min-width: $maxWidth) {
16 | width: 50%;
17 | }
18 | }
19 | a {
20 | margin-bottom: $rhythm;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 | #
3 | # If you find yourself ignoring temporary files generated by your text editor
4 | # or operating system, you probably want to add a global ignore instead:
5 | # git config --global core.excludesfile ~/.gitignore_global
6 |
7 | # Ignore bundler config
8 | /.bundle
9 |
10 | # Ignore the default SQLite database.
11 | /db/*.sqlite3
12 |
13 | # Ignore all logfiles and tempfiles.
14 | /log
15 | /tmp
16 |
17 | # SimpleCov reports
18 | coverage
19 |
20 | # uploads
21 | public/uploads
22 |
--------------------------------------------------------------------------------
/app/assets/javascripts/components/datetime_picker.coffee:
--------------------------------------------------------------------------------
1 | format = 'M/D/YYYY h:mma'
2 |
3 | $ ->
4 | $('input.datetime_picker').each ->
5 | $input = $(@)
6 | $hiddenInput = $(" ")
7 | $hiddenInput.insertAfter($input)
8 | $input.attr('name', null)
9 | rome(
10 | @,
11 | inputFormat: format,
12 | timeFormat: "h:mma",
13 | timeInterval: 3600 # hour
14 | initialValue: new Date($input.val())
15 | ).on 'data', ->
16 | $hiddenInput.val moment($input.val(), format).toDate()
17 |
--------------------------------------------------------------------------------
/app/views/home/_about.html.erb:
--------------------------------------------------------------------------------
1 | <% # This page shouldn't be translated, since it will vary from site to site. %>
2 |
3 |
4 |
7 |
8 | <%= DispatchConfiguration.site_title %> is a website that lists contracting opportunities
9 | from <%= DispatchConfiguration.agency_name %>. It's powered by an
open-source project from The Department of Better Technology.
10 |
11 |
--------------------------------------------------------------------------------
/app/views/users/confirmations/new.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for(:page_title, 'Resend confirmation instructions') %>
2 | <% content_for(:main_container_class, 'container_tiny') %>
3 |
4 |
7 |
8 | <%= simple_form_for resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post } do |f| %>
9 | <%= f.full_error :confirmation_token %>
10 | <%= f.input :email, required: true, autofocus: true %>
11 | <%= f.button :submit, "Resend", class: 'primary' %>
12 | <% end %>
13 |
--------------------------------------------------------------------------------
/app/views/submission_adapters/screendoor/_submit.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :extra_css do %>
2 |
3 | <% end %>
4 |
5 | <% content_for :extra_js do %>
6 |
7 |
11 | <% end %>
12 |
13 |
14 |
--------------------------------------------------------------------------------
/spec/factories/departments.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: departments
4 | #
5 | # id :integer not null, primary key
6 | # name :string
7 | # created_at :datetime not null
8 | # updated_at :datetime not null
9 | #
10 |
11 | FactoryGirl.define do
12 | factory :department do
13 | sequence(:name) do |i|
14 | names = [
15 | 'Office of Management and Budget',
16 | 'Office of Innovation and Technology',
17 | "Mayor's Office"
18 | ]
19 |
20 | names[i % names.length]
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/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] if respond_to?(:wrap_parameters)
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 |
--------------------------------------------------------------------------------
/app/models/audit.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: audits
4 | #
5 | # id :integer not null, primary key
6 | # user_id :integer not null
7 | # event :string
8 | # data :text
9 | # created_at :datetime not null
10 | # updated_at :datetime not null
11 | #
12 | # Indexes
13 | #
14 | # index_audits_on_event (event)
15 | # index_audits_on_user_id (user_id)
16 | # index_audits_on_user_id_and_event (user_id,event)
17 | #
18 |
19 | class Audit < ActiveRecord::Base
20 | belongs_to :user
21 | serialize :data, Hash
22 | end
23 |
--------------------------------------------------------------------------------
/app/views/users/registrations/_my_opportunities.erb:
--------------------------------------------------------------------------------
1 | <% if (opps = current_user.created_opportunities.order_by_recently_updated).present? %>
2 |
5 |
6 |
16 | <% end %>
17 |
--------------------------------------------------------------------------------
/app/models/saved_search.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: saved_searches
4 | #
5 | # id :integer not null, primary key
6 | # user_id :integer
7 | # search_params :text
8 | # created_at :datetime not null
9 | # updated_at :datetime not null
10 | #
11 | # Indexes
12 | #
13 | # index_saved_searches_on_user_id (user_id)
14 | #
15 |
16 | class SavedSearch < ActiveRecord::Base
17 | belongs_to :user
18 | serialize :search_params, Hash
19 |
20 | PERMITTED_SEARCH_PARAMS = [
21 | :text,
22 | :status,
23 | :category_ids
24 | ].freeze
25 | end
26 |
--------------------------------------------------------------------------------
/themes/dvl-core/assets/stylesheets/theme/dvl_core.scss:
--------------------------------------------------------------------------------
1 | @import 'theme/includes';
2 | @import 'dvl/core';
3 | @import 'dvl/components/navbar';
4 | @import 'dvl/components/flashes';
5 | @import 'dvl/components/hero';
6 | @import 'dvl/components/page_header';
7 | @import 'dvl/components/page_subheader';
8 | @import 'dvl/components/sidebar_sub_actions';
9 | @import 'dvl/components/selectize';
10 | @import 'dvl/components/alerts';
11 | @import 'dvl/components/sidebar_data';
12 | @import 'dvl/components/tabs';
13 | @import 'dvl/components/blank_slate';
14 | @import 'dvl/components/footer';
15 | @import 'dvl/components/pagination';
16 |
--------------------------------------------------------------------------------
/themes/dvl-core/assets/stylesheets/theme/overrides/page_header.scss:
--------------------------------------------------------------------------------
1 | @import 'theme/includes';
2 |
3 | .page_header_simple h2 {
4 | text-align: center;
5 | float: none;
6 | margin: 0 0 ($rhythm * 1.5);
7 | }
8 |
9 | .page_header_action {
10 | display: table;
11 | padding-bottom: $rhythm * 1.5;
12 | h2, .page_header_action_button {
13 | float: none;
14 | display: table-cell;
15 | }
16 | h2 {
17 | width: 100%;
18 | padding-right: $rhythm * 2;
19 | }
20 | .page_header_action_button {
21 | width: 1px;
22 | .button {
23 | position: relative;
24 | top: $rhythm;
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/spec/features/home_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'Home' do
4 | describe 'recent opportunities' do
5 | let!(:visible_opportunity) { create(:opportunity, :published, :approved) }
6 | let!(:not_visible_opportunity) { create(:opportunity, :published) }
7 |
8 | it 'shows posted opportunities only' do
9 | visit root_path
10 |
11 | expect(page).to have_selector(
12 | %([href="#{opportunity_path(visible_opportunity)}"])
13 | )
14 |
15 | expect(page).to_not have_selector(
16 | %([href="#{opportunity_path(not_visible_opportunity)}"])
17 | )
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/submission_adapters/screendoor.rb:
--------------------------------------------------------------------------------
1 | module SubmissionAdapters
2 | class Screendoor < SubmissionAdapters::Base
3 | self.select_text = 'Screendoor'
4 | self.submission_page = true
5 |
6 | def view_proposals_url
7 | "https://screendoor.dobt.co/projects/#{embed_token}/admin"
8 | end
9 |
10 | def view_proposals_link_text
11 | "Review submissions ".html_safe
12 | end
13 |
14 | def valid?
15 | embed_token.present?
16 | end
17 |
18 | private
19 |
20 | def embed_token
21 | @opportunity.submission_adapter_data['embed_token']
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/app/views/home/_recent_opportunities.html.erb:
--------------------------------------------------------------------------------
1 | <% if @recent_opportunities.present? %>
2 |
17 | <% end %>
18 |
--------------------------------------------------------------------------------
/docs/developing_dispatch_core.md:
--------------------------------------------------------------------------------
1 | Developing Dispatch Core
2 | ---
3 |
4 | > Developing "Dispatch Core" is different than [customizing](customization.md) your instance of Dispatch. Before making changes to the core application, check to see if your use case is covered in our [customization docs](customization.md) first.
5 |
6 | ## Some general rules of thumb
7 |
8 | - `dvl-core` is the default theme, so the views in `app/views/` use class names for that theme
9 | - Strings should be internationalized. Run `i18n-tasks normalize` before committing in order to sort the keys inside of `en.yml` properly
10 | - Don't add too much JavaScript -- this will make it harder to customize the app
11 |
--------------------------------------------------------------------------------
/app/views/opportunities/feed.xml.builder:
--------------------------------------------------------------------------------
1 | xml.instruct! :xml, version: "1.0"
2 | xml.rss version: "2.0" do
3 | xml.channel do
4 | xml.title "Opportunities - #{DispatchConfiguration.site_title}"
5 | xml.description "Feed of opportunities posted to #{DispatchConfiguration.site_title}."
6 | xml.link opportunities_url
7 |
8 | for opportunity in @opportunities
9 | xml.item do
10 | xml.title opportunity.title
11 | xml.description opportunity.description
12 | xml.pubDate opportunity.updated_at.to_s(:rfc822)
13 | xml.link opportunity_url(opportunity)
14 | xml.guid opportunity_url(opportunity)
15 | end
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/config/initializers/carrierwave.rb:
--------------------------------------------------------------------------------
1 | CarrierWave.configure do |config|
2 | if DispatchConfiguration.upload_storage == 'aws'
3 | config.storage = :aws
4 | config.cache_dir = Rails.root.join('tmp/uploads')
5 | config.aws_credentials = {
6 | access_key_id: DispatchConfiguration.aws_key,
7 | secret_access_key: DispatchConfiguration.aws_secret,
8 | region: DispatchConfiguration.aws_region
9 | }
10 | config.aws_bucket = DispatchConfiguration.aws_bucket
11 | config.aws_attributes = {
12 | cache_control: 'max-age=315576000'
13 | }
14 | config.aws_authenticated_url_expiration = 60 * 60 # one hour
15 | else
16 | config.storage = :file
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/app/views/opportunities/_subscribe_button.html.erb:
--------------------------------------------------------------------------------
1 | <% if policy(@opportunity).subscribe? %>
2 |
3 | <% if current_user.opportunities.include?(@opportunity) %>
4 |
5 | <%= t('reminded') %>
6 | <% else %>
7 |
8 | <%= t('remind_me') %>
9 | <% end %>
10 |
11 | <% elsif OpportunityPolicy.new(User.new, @opportunity).subscribe? %>
12 | <% # @todo redirect after login %>
13 | <%= t('remind_me') %>
14 | <% end %>
15 |
--------------------------------------------------------------------------------
/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | # inflect.plural /^(ox)$/i, '\1en'
8 | # inflect.singular /^(ox)en/i, '\1'
9 | # inflect.irregular 'person', 'people'
10 | # inflect.uncountable %w( fish sheep )
11 | # end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | # inflect.acronym 'RESTful'
16 | # end
17 |
--------------------------------------------------------------------------------
/config/initializers/theme.rb:
--------------------------------------------------------------------------------
1 | def require_if_exists(path)
2 | require(path) if File.exist?(path)
3 | end
4 |
5 | # Require theme's simple_form configuration
6 | require_if_exists DispatchConfiguration.theme_path.join('simple_form.rb')
7 |
8 | # Add theme assets to the beginning of the sprockets load path
9 | Rails.configuration.assets.paths =
10 | Dir[DispatchConfiguration.theme_path.join("assets/*")] +
11 | Rails.configuration.assets.paths
12 |
13 | # Add theme i18n
14 | Rails.configuration.i18n.load_path +=
15 | Dir[DispatchConfiguration.theme_path.join('locales/**/*')]
16 |
17 | # Load theme's submission adapters
18 | Dir[DispatchConfiguration.theme_path.join('submission_adapters/**/*')].each do |f|
19 | require f
20 | end
21 |
--------------------------------------------------------------------------------
/lib/submission_adapters/email.rb:
--------------------------------------------------------------------------------
1 | module SubmissionAdapters
2 | class Email < SubmissionAdapters::Base
3 | self.select_text = 'Email'
4 |
5 | def submit_proposals_instructions
6 | %(
7 | Proposals for this opportunity should be sent by email to
8 | #{submit_to_name} .
9 | ).squish.html_safe
10 | end
11 |
12 | def valid?
13 | submit_to_email.present? &&
14 | submit_to_name.present?
15 | end
16 |
17 | private
18 |
19 | def submit_to_email
20 | @opportunity.submission_adapter_data['email']
21 | end
22 |
23 | def submit_to_name
24 | @opportunity.submission_adapter_data['name']
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/app/views/application/_footer.html.erb:
--------------------------------------------------------------------------------
1 |
22 |
--------------------------------------------------------------------------------
/app/views/opportunities/_view_attachment.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <% if attachment.has_thumbnail? %>
3 |
4 |
5 |
6 | <% else %>
7 |
8 |
9 | <%= attachment.upload.friendly_file_type %>
10 |
11 | <% end %>
12 |
13 |
14 | <%= attachment.upload.raw_filename %>
15 | <%= number_to_human_size(attachment.file_size_bytes)%>
16 |
17 |
18 |
--------------------------------------------------------------------------------
/spec/models/question_spec.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: questions
4 | #
5 | # id :integer not null, primary key
6 | # opportunity_id :integer
7 | # asked_by_user_id :integer
8 | # answered_by_user_id :integer
9 | # question_text :text
10 | # answer_text :text
11 | # answered_at :datetime
12 | # deleted_at :datetime
13 | # created_at :datetime not null
14 | # updated_at :datetime not null
15 | #
16 | # Indexes
17 | #
18 | # index_questions_on_opportunity_id (opportunity_id)
19 | #
20 |
21 | require 'spec_helper'
22 |
23 | describe Question do
24 | subject { build(:question) }
25 | it { should be_valid }
26 | end
27 |
--------------------------------------------------------------------------------
/app/views/opportunities/_contact_info.html.erb:
--------------------------------------------------------------------------------
1 | <% if @opportunity.contact_info? %>
2 |
20 | <% end %>
21 |
--------------------------------------------------------------------------------
/config/schedule.rb:
--------------------------------------------------------------------------------
1 | # Use this file to easily define all of your cron jobs.
2 | #
3 | # It's helpful, but not entirely necessary to understand cron before proceeding.
4 | # http://en.wikipedia.org/wiki/Cron
5 |
6 | # Example:
7 | #
8 | # set :output, "/path/to/my/cron_log.log"
9 | #
10 | # every 2.hours do
11 | # command "/usr/bin/some_great_command"
12 | # runner "MyModel.some_method"
13 | # rake "some:great:rake:task"
14 | # end
15 | #
16 | # every 4.days do
17 | # runner "AnotherModel.prune_old_records"
18 | # end
19 |
20 | # Learn more: http://github.com/javan/whenever
21 |
22 | every 1.week do
23 | runner 'QueueUserSearchResultsJob.perform_later'
24 | end
25 |
26 | every 1.hour do
27 | runner 'SendDeadlineRemindersJob.perform_later'
28 | end
29 |
--------------------------------------------------------------------------------
/lib/dispatch_configuration.rb:
--------------------------------------------------------------------------------
1 | module DispatchConfiguration
2 | class << self
3 | def theme_path
4 | Rails.root.join("themes/#{theme}")
5 | end
6 |
7 | def method_missing(name)
8 | ENV[name.to_s.upcase] ||
9 | read_configuration[name.to_s.upcase]
10 | end
11 |
12 | private
13 |
14 | def read_configuration
15 | @configuration ||= YAML.safe_load(File.read(config_file))
16 | end
17 |
18 | def config_file
19 | if File.exist?(Rails.root.join('config.yml'))
20 | Rails.root.join('config.yml')
21 | else
22 | # For development modes and the like, just use the defaults in the
23 | # example file
24 | Rails.root.join('config.yml.example')
25 | end
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/config/environments/_smtp_env_vars.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | config.action_mailer.delivery_method = :smtp
3 | config.action_mailer.smtp_settings = {
4 | address: DispatchConfiguration.smtp_address,
5 | port: (DispatchConfiguration.smtp_port.presence || 465).to_i,
6 | enable_starttls_auto: DispatchConfiguration.smtp_starttls_auto == '1',
7 | user_name: DispatchConfiguration.smtp_user.presence,
8 | password: DispatchConfiguration.smtp_password.presence,
9 | authentication: DispatchConfiguration.smtp_authentication.presence.
10 | try(:to_sym),
11 | domain: DispatchConfiguration.smtp_domain.presence ||
12 | DispatchConfiguration.base_domain,
13 | ssl: DispatchConfiguration.smtp_ssl == '1'
14 | }
15 | end
16 |
--------------------------------------------------------------------------------
/themes/dvl-core/assets/stylesheets/theme/layout/application.scss:
--------------------------------------------------------------------------------
1 | @import 'theme/includes';
2 |
3 | .container {
4 | @include container;
5 |
6 | &.container_small {
7 | max-width: 50rem;
8 | }
9 |
10 | &.container_tiny {
11 | max-width: 35rem;
12 | }
13 | }
14 |
15 | .main_container {
16 | padding-top: $rhythm * 3;
17 | }
18 |
19 | .opportunity_section {
20 | margin-bottom: $rhythm * 4;
21 |
22 | form {
23 | max-width: 36rem;
24 | }
25 | }
26 |
27 | .page_subheader_simple {
28 | margin-top: $lineHeight;
29 | h3 {
30 | text-align: center;
31 | float: none;
32 | display: block;
33 | }
34 | }
35 |
36 | .footer {
37 | margin-top: $lineHeight * 2;
38 | }
39 |
40 | th a {
41 | color: $darkerGray;
42 | white-space: nowrap;
43 | }
44 |
--------------------------------------------------------------------------------
/app/uploaders/base_uploader.rb:
--------------------------------------------------------------------------------
1 | class BaseUploader < CarrierWave::Uploader::Base
2 | self.aws_acl = :private
3 |
4 | def model_class_name
5 | model.class.to_s
6 | end
7 |
8 | def store_dir
9 | "uploads/#{store_digest}"
10 | end
11 |
12 | def store_digest
13 | Digest::SHA2.hexdigest(
14 | "#{model_class_name.underscore}-#{mounted_as}-#{model.id}"
15 | ).first(32)
16 | end
17 |
18 | def raw_filename
19 | @model.read_attribute(mounted_as)
20 | end
21 |
22 | def friendly_file_type
23 | if (ext = File.extname(raw_filename)).present?
24 | ext[1..-1].upcase # Remove '.' from string
25 | else
26 | '?'
27 | end
28 | end
29 |
30 | def download_url
31 | url(response_content_disposition: 'attachment')
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/app/views/users/registrations/_edit_password.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for(:page_title, 'Change password') %>
2 | <% content_for(:main_container_class, 'container_tiny') %>
3 |
4 |
7 |
8 | <%= simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
9 |
10 |
11 | <%= f.input :current_password, label: "Current password", required: true, autofocus: true %>
12 | <%= f.input :password, label: "New password", required: true %>
13 | <%= f.input :password_confirmation, label: "Confirm your new password", required: true %>
14 |
15 | <%= f.button :button, 'Change your password' %>
16 | <% end %>
17 |
--------------------------------------------------------------------------------
/app/models/attachment.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: attachments
4 | #
5 | # id :integer not null, primary key
6 | # opportunity_id :integer
7 | # upload :string
8 | # content_type :string
9 | # file_size_bytes :integer
10 | # has_thumbnail :boolean default(FALSE), not null
11 | # deleted_at :datetime
12 | # created_at :datetime not null
13 | # updated_at :datetime not null
14 | # text_content :text
15 | #
16 | # Indexes
17 | #
18 | # index_attachments_on_opportunity_id (opportunity_id)
19 | #
20 |
21 | class Attachment < ActiveRecord::Base
22 | belongs_to :opportunity
23 |
24 | has_storage_unit
25 |
26 | mount_uploader :upload, AttachmentUploader
27 | validates :upload, presence: true
28 | end
29 |
--------------------------------------------------------------------------------
/app/views/users/passwords/edit.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for(:page_title, 'Change your password') %>
2 | <% content_for(:main_container_class, 'container_tiny') %>
3 |
4 |
7 |
8 | <%= simple_form_for resource, as: resource_name, url: password_path(resource_name), html: { method: :put } do |f| %>
9 | <%= f.input :reset_password_token, as: :hidden %>
10 | <%= f.full_error :reset_password_token %>
11 | <%= f.input :password, label: "New password", required: true, autofocus: true, hint: ("#{@minimum_password_length} characters minimum" if @minimum_password_length) %>
12 | <%= f.input :password_confirmation, label: "Confirm your new password", required: true %>
13 | <%= f.button :submit, "Change my password", class: 'primary' %>
14 | <% end %>
15 |
--------------------------------------------------------------------------------
/spec/factories/categories.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: categories
4 | #
5 | # id :integer not null, primary key
6 | # name :string
7 | # created_at :datetime not null
8 | # updated_at :datetime not null
9 | #
10 |
11 | FactoryGirl.define do
12 | factory :category do
13 | sequence(:name) do |i|
14 | names = [
15 | 'Business Intelligence & Analytics',
16 | 'Consulting',
17 | 'Creative Services',
18 | 'CRM',
19 | 'Data Management',
20 | 'Database',
21 | 'General',
22 | 'Mobile Apps',
23 | 'Process Improvement',
24 | 'Support',
25 | 'Web Apps',
26 | 'Web Design'
27 | ]
28 |
29 | names[i % names.length]
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/app/controllers/saved_searches_controller.rb:
--------------------------------------------------------------------------------
1 | class SavedSearchesController < ApplicationController
2 | include PickHelper
3 |
4 | before_action :authenticate_user!
5 | before_action :set_saved_search
6 | before_action :skip_authorization
7 |
8 | def create
9 | current_user.saved_searches.create(search_params: saved_search_params)
10 | redirect_to :back, success: t('filter_saved')
11 | end
12 |
13 | def destroy
14 | @saved_search.destroy
15 | redirect_to :back, info: t('filter_destroyed')
16 | end
17 |
18 | private
19 |
20 | def saved_search_params
21 | pick(params, *SavedSearch::PERMITTED_SEARCH_PARAMS)
22 | end
23 |
24 | def set_saved_search
25 | if params[:id]
26 | @saved_search = current_user.saved_searches.find(params[:id])
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/app/views/opportunities/edit/_submission_adapter_inputs.html.erb:
--------------------------------------------------------------------------------
1 | <% SubmissionAdapters.all_adapters.each do |x| %>
2 | <% if lookup_context.exists?(submission_adapter_edit_partial(x), nil, true) %>
3 | style='display:none'<% end %>>
4 | <%= f.simple_fields_for :submission_adapter_data, OpenStruct.new(f.object.submission_adapter_data) do |f| %>
5 | <%= render(partial: submission_adapter_edit_partial(x), locals: { f: f }) %>
6 | <% end %>
7 |
8 | <% if f.object.submission_adapter_name == x.to_adapter_name %>
9 |
10 | <%= f.error :submission_adapter %>
11 |
12 | <% end %>
13 |
14 | <% end %>
15 | <% end %>
16 |
--------------------------------------------------------------------------------
/Guardfile:
--------------------------------------------------------------------------------
1 | # rubocop:disable all
2 | guard :rspec, all_on_start: false, all_after_pass: false, failed_mode: :focus, cmd: 'bin/rspec' do
3 | watch(%r{^spec/.+_spec\.rb$})
4 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
5 | watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
6 | watch(%r{^app/controllers/api/(.+)\.rb$}) { |m| "spec/api/#{m[1]}_spec.rb" }
7 | watch(%r{^app/views/(.+)\.rb$}) { |m| "spec/views/#{m[1]}_spec.rb" }
8 | end
9 |
10 | guard :livereload do
11 | watch(%r{app/views/.+\.(erb)$})
12 | watch(%r{themes/(.+)/assets/\w+/(.+)\.(scss)})
13 | watch(%r{(app|vendor)(/assets/\w+/(.+)\.(scss))})
14 | end
15 |
16 | guard :rubocop, all_on_start: false do
17 | watch(%r{.+\.rb$})
18 | watch(%r{(?:.+/)?\.rubocop\.yml$}) { |m| File.dirname(m[0]) }
19 | end
20 |
--------------------------------------------------------------------------------
/spec/support/utilities.rb:
--------------------------------------------------------------------------------
1 | # Show file inputs that are hidden and replaced by a
2 | def show_hidden_file_inputs
3 | page.execute_script %{
4 | $('input[type=file]').css({
5 | position: 'static',
6 | top: '0',
7 | left: '0',
8 | display: 'block',
9 | clear: 'both'
10 | })
11 | }
12 | end
13 |
14 | def wait_until
15 | require 'timeout'
16 | Timeout.timeout(Capybara.default_max_wait_time) do
17 | sleep(0.1) until (value = yield)
18 | value
19 | end
20 | end
21 |
22 | def wait_for_ajax
23 | wait_until do
24 | page.evaluate_script('$.active') == 0
25 | end
26 | end
27 |
28 | def with_invisible_elements
29 | original_config_val = Capybara.ignore_hidden_elements
30 | Capybara.ignore_hidden_elements = false
31 | yield
32 | Capybara.ignore_hidden_elements = original_config_val
33 | end
34 |
--------------------------------------------------------------------------------
/themes/dvl-core/assets/stylesheets/theme/components/opportunity.scss:
--------------------------------------------------------------------------------
1 | @import 'theme/includes';
2 |
3 | .opportunity {
4 | margin-bottom: $rhythm * 4;
5 | a {
6 | display: block;
7 | border: 1px solid $gray;
8 | box-shadow: 0 1px 0 rgba(#000,0.06);
9 | border-radius: $radius;
10 | padding: ($rhythm * 2) ($rhythm * 3);
11 | transition: background-color 0.15s ease-out;
12 | &:hover,
13 | &:focus,
14 | &:active {
15 | text-decoration: none;
16 | outline: 0;
17 | }
18 | &:hover,
19 | &:focus {
20 | background: $lighterGray;
21 | }
22 | }
23 | span {
24 | color: $primaryColor;
25 | font-size: 1.2rem;
26 | @include font_smoothing;
27 | }
28 | em {
29 | font-style: normal;
30 | color: $darkerGray;
31 | font-size: $fontSmall;
32 | display: block;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/jobs/send_deadline_reminders_job.rb:
--------------------------------------------------------------------------------
1 | class SendDeadlineRemindersJob < ApplicationJob
2 | def perform
3 | send_question_reminders
4 | send_submission_reminders
5 | end
6 |
7 | private
8 |
9 | def send_question_reminders
10 | Opportunity.needs_question_deadline_reminders.each do |opportunity|
11 | opportunity.users.each do |user|
12 | Mailer.question_deadline(user, opportunity).deliver_later
13 | end
14 |
15 | opportunity.update(question_deadline_reminder_sent: true)
16 | end
17 | end
18 |
19 | def send_submission_reminders
20 | Opportunity.needs_submission_deadline_reminders.each do |opportunity|
21 | opportunity.users.each do |user|
22 | Mailer.submission_deadline(user, opportunity).deliver_later
23 | end
24 |
25 | opportunity.update(submission_deadline_reminder_sent: true)
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/app/views/opportunities/show.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for(:page_title, @opportunity.title) %>
2 |
3 | <% if !@opportunity.posted? %>
4 | <%= render('admin_status') %>
5 | <% end %>
6 |
7 |
16 |
17 |
18 |
19 | <%= render('description') %>
20 | <%= render('attachments') %>
21 | <%= render('questions') if @opportunity.enable_questions? %>
22 |
23 |
24 | <%= render('timeline') %>
25 | <%= render('vendor_actions') %>
26 | <%= render('admin_links') %>
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/views/opportunities/edit/_submissions.erb:
--------------------------------------------------------------------------------
1 | <%= f.input :submission_adapter_name do %>
2 | <%= f.input_field :submission_adapter_name, as: :select, collection: SubmissionAdapters.all_adapters, include_blank: false, value_method: :to_adapter_name, label_method: :select_text %>
3 |
4 | style='display:none'<% end %>>
5 | <%= t('no_submission_adapter_warning').html_safe %>
6 |
7 | <% end %>
8 |
9 | <%= render partial: 'opportunities/edit/submission_adapter_inputs', locals: { f: f } %>
10 |
11 | <%= f.input :submission_dates, wrapper_html: { class: 'date_range' } do %>
12 | <%= f.input_field :submissions_open_at, as: :datetime_picker %>
13 | and
14 | <%= f.input_field :submissions_close_at, as: :datetime_picker %>
15 | <% end %>
16 |
--------------------------------------------------------------------------------
/app/assets/javascripts/components/progress_guard.coffee:
--------------------------------------------------------------------------------
1 | class ProgressGuard
2 | constructor: ($el, options) ->
3 | @hasChanges = false
4 | @$el = $el
5 |
6 | @$el.on 'input', ':input', @changed
7 | @$el.on 'change', 'select', @changed
8 | @$el.on 'click', 'input[type=radio], input[type=checkbox]', @changed
9 | @$el.on 'submit', @clear
10 |
11 | window.onbeforeunload = =>
12 | if @hasChanges then "You have unsaved changes" else undefined
13 |
14 | changed: =>
15 | @hasChanges = true
16 |
17 | clear: =>
18 | @hasChanges = false
19 | return # Don't return false!
20 |
21 | $.fn.extend progressGuard: (option, args...) ->
22 | @each ->
23 | data = $(@).data('progress-guard')
24 |
25 | if !data
26 | $(@).data 'progress-guard', (data = new ProgressGuard($(@), option))
27 | if typeof option == 'string'
28 | data[option].apply(data, args)
29 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the top of the
9 | * compiled file, but it's generally better to create a new file per style scope.
10 | *
11 | *= require_self
12 | *= require font-awesome
13 | *= require selectize
14 | *= require rome
15 | *= require theme
16 | */
17 |
18 | /* Prevent unstyled flash while Selectize loads */
19 | select[multiple] {
20 | display: none;
21 | }
22 |
23 | /* Fix rome / selectize z-index conflict */
24 | .rd-container {
25 | z-index: 2;
26 | }
27 |
--------------------------------------------------------------------------------
/app/views/static/about.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for(:page_title, 'About this site') %>
2 | <% content_for(:main_container_class, 'container_tiny') %>
3 |
4 |
7 |
8 |
9 | <%= DispatchConfiguration.site_title %> is a website that lists contracting opportunities
10 | from <%= DispatchConfiguration.agency_name %>. If you're a business, you can browse opportunities that have been posted. '>Creating an account also lets you receive email notifications of new opportunities that are a good fit for your business.
11 |
12 |
13 |
14 | If you're a government employee, you can '>create an account to post new opportunities, and approve submitted opportunities.
15 |
16 |
--------------------------------------------------------------------------------
/app/assets/javascripts/application.js:
--------------------------------------------------------------------------------
1 | // This is a manifest file that'll be compiled into application.js, which will include all the files
2 | // listed below.
3 | //
4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
6 | //
7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8 | // compiled file.
9 | //
10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
11 | // about supported directives.
12 | //
13 | //= require jquery-ujs
14 | //= require jquery-form
15 | //= require jquery-timeago
16 | //= require inline_file_upload
17 | //= require selectize/standalone/selectize
18 | //= require moment
19 | //= require rome/rome.standalone.js
20 | //= require theme
21 | //= require_tree .
22 |
--------------------------------------------------------------------------------
/app/views/users/registrations/_saved_searches.html.erb:
--------------------------------------------------------------------------------
1 | <% if current_user.saved_searches.present? %>
2 |
5 |
6 |
24 | <% end %>
25 |
--------------------------------------------------------------------------------
/docs/setting_up_a_development_environment.md:
--------------------------------------------------------------------------------
1 | Setting up a development environment
2 | ----
3 |
4 | ## Installation
5 |
6 | You'll need to first install the following:
7 |
8 | - Ruby 2.3.1
9 | - Bundler
10 | - node.js
11 | - Postgres
12 | - imagemagick
13 | - qt
14 |
15 | Then, run `script/bootstrap` to install gems and seed your database.
16 |
17 | ## Development
18 |
19 | Run `script/server` and navigate to http://localhost:3000.
20 |
21 | We automatically generate a dummy user account. You can login as `admin@example.com` with the password `password`.
22 |
23 | ## Testing
24 |
25 | Make sure you have a test database by running `rake db:test:prepare`. Then, either use `bin/guard`, which will watch for changes and run tests automatically, or run `rspec` directly.
26 |
27 | (If you're on Linux, you'll need to [manually run an X server](https://github.com/thoughtbot/capybara-webkit/blob/v1.3.0/README.md#ci) before running javascript-enabled specs.)
28 |
--------------------------------------------------------------------------------
/app/views/users/registrations/_new_staff.erb:
--------------------------------------------------------------------------------
1 | <% content_for(:page_title, t('.title', agency: DispatchConfiguration.agency_name)) %>
2 | <% content_for(:main_container_class, 'container_tiny') %>
3 |
4 |
7 |
8 |
9 |
10 |
11 | <%= simple_form_for resource, as: resource_name, url: registration_path(resource_name) do |f| %>
12 |
13 |
14 | <%= f.input :name, label: t('.name') %>
15 | <%= f.input :email, label: t('.email', domain: DispatchConfiguration.staff_domains.map { |x| "@#{x}" }.to_sentence(words_connector: ' or ', two_words_connector: ' or ', last_word_connector: ', or ')).html_safe, required: true %>
16 | <%= f.input :password, label: t('.password'), hint: t('password_hint'), required: true %>
17 |
18 | <%= f.button :button, t('sign_up'), class: 'primary' %>
19 | <% end %>
20 |
--------------------------------------------------------------------------------
/config/locales/simple_form.en.yml:
--------------------------------------------------------------------------------
1 | en:
2 | simple_form:
3 | "yes": 'Yes'
4 | "no": 'No'
5 | required:
6 | text: 'required'
7 | mark: '*'
8 | # You can uncomment the line below if you need to overwrite the whole required html.
9 | # When using html, text and mark won't be used.
10 | # html: '* '
11 | error_notification:
12 | default_message: "Please fix the errors below."
13 | # Examples
14 | # labels:
15 | # defaults:
16 | # password: 'Password'
17 | # user:
18 | # new:
19 | # email: 'E-mail to sign in.'
20 | # edit:
21 | # email: 'E-mail.'
22 | # hints:
23 | # defaults:
24 | # username: 'User name to sign in.'
25 | # password: 'No special characters, please.'
26 | # include_blanks:
27 | # defaults:
28 | # age: 'Rather not say'
29 | # prompts:
30 | # defaults:
31 | # age: 'Select your age'
32 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require 'codeclimate-test-reporter'
2 | CodeClimate::TestReporter.start
3 |
4 | SimpleCov.start 'rails' do
5 | formatter SimpleCov::Formatter::MultiFormatter.new(
6 | [
7 | SimpleCov::Formatter::HTMLFormatter,
8 | CodeClimate::TestReporter::Formatter
9 | ]
10 | )
11 | end
12 |
13 | ENV['RAILS_ENV'] = 'test'
14 | require File.expand_path('../../config/environment', __FILE__)
15 | require 'rspec/rails'
16 | require 'rspec/its'
17 | require 'capybara/rspec'
18 |
19 | Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
20 | ActiveRecord::Migration.maintain_test_schema!
21 |
22 | Warden.test_mode!
23 |
24 | RSpec.configure do |c|
25 | c.infer_spec_type_from_file_location!
26 | c.use_transactional_fixtures = false
27 | c.order = 'random'
28 |
29 | c.include FactoryGirl::Syntax::Methods
30 | c.include AbstractController::Translation
31 | c.include Warden::Test::Helpers
32 | end
33 |
34 | Capybara.javascript_driver = :webkit
35 |
--------------------------------------------------------------------------------
/spec/factories/attachments.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: attachments
4 | #
5 | # id :integer not null, primary key
6 | # opportunity_id :integer
7 | # upload :string
8 | # content_type :string
9 | # file_size_bytes :integer
10 | # has_thumbnail :boolean default(FALSE), not null
11 | # deleted_at :datetime
12 | # created_at :datetime not null
13 | # updated_at :datetime not null
14 | #
15 | # Indexes
16 | #
17 | # index_attachments_on_opportunity_id (opportunity_id)
18 | #
19 |
20 | FactoryGirl.define do
21 | factory :attachment do
22 | opportunity
23 |
24 | upload { File.open(Rails.root.join('spec/fixtures/files/test.txt')) }
25 |
26 | trait :docx do
27 | upload { File.open(Rails.root.join('spec/fixtures/files/test.docx')) }
28 | end
29 |
30 | trait :empty do
31 | upload { File.open(Rails.root.join('spec/fixtures/files/empty')) }
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/spec/factories/questions.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: questions
4 | #
5 | # id :integer not null, primary key
6 | # opportunity_id :integer
7 | # asked_by_user_id :integer
8 | # answered_by_user_id :integer
9 | # question_text :text
10 | # answer_text :text
11 | # answered_at :datetime
12 | # deleted_at :datetime
13 | # created_at :datetime not null
14 | # updated_at :datetime not null
15 | #
16 | # Indexes
17 | #
18 | # index_questions_on_opportunity_id (opportunity_id)
19 | #
20 |
21 | FactoryGirl.define do
22 | factory :question do
23 | opportunity
24 | association :asked_by_user, factory: :user
25 | question_text 'Are you coding language agnostic?'
26 |
27 | trait :answered do
28 | association :answered_by_user, factory: :user
29 | answer_text 'Yes. coding language agnostic.'
30 | answered_at { Time.now }
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/app/views/opportunities/index.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for(:page_key, 'opportunities-index') %>
2 | <% content_for(:page_title, t('browse_opportunities')) %>
3 |
4 |
21 |
22 |
23 |
24 | <%= render 'filter_form' %>
25 |
26 |
27 | <%= render 'search_results' %>
28 |
29 |
30 |
--------------------------------------------------------------------------------
/app/assets/javascripts/components/attachment_upload.coffee:
--------------------------------------------------------------------------------
1 | $(document).on 'ajax:beforeSend', '.js-remove-attachment', ->
2 | $(@).closest('li').remove()
3 |
4 | $ ->
5 | return unless $('#attachment_upload')[0]
6 |
7 | $error = $('#attachment_upload').closest('.form_item').find('.js-upload-error')
8 | $label = $('label[for=attachment_upload]')
9 | originalText = $label.text()
10 |
11 | $('#attachment_upload').inlineFileUpload
12 | start: ->
13 | $label.addClass('disabled')
14 |
15 | progress: (data) ->
16 | $label.text(
17 | if data.percent == 100
18 | 'Finishing...'
19 | else
20 | "Uploading (#{data.percent}%)"
21 | )
22 |
23 | complete: ->
24 | $label.html(originalText).removeClass('disabled')
25 |
26 | success: (data) ->
27 | $(data.data.html).appendTo('.js-attachments-list')
28 |
29 | error: (data) ->
30 | $error.text(data.xhr.responseJSON?.error || 'Error').show()
31 | setTimeout ->
32 | $error.hide()
33 | , 3000
34 |
--------------------------------------------------------------------------------
/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | config.cache_store = :memory_store
3 |
4 | config.cache_classes = false
5 | config.eager_load = false
6 |
7 | # Show full error reports and disable caching.
8 | config.consider_all_requests_local = true
9 | config.action_controller.perform_caching = true
10 |
11 | # Print deprecation notices to the Rails logger.
12 | config.active_support.deprecation = :log
13 |
14 | # Raise an error on page load if there are pending migrations
15 | config.active_record.migration_error = :page_load
16 |
17 | # Debug mode disables concatenation and preprocessing of assets.
18 | # This option may cause significant delays in view rendering with a large
19 | # number of complex assets.
20 | config.assets.debug = false
21 | config.assets.digest = false
22 |
23 | config.action_mailer.raise_delivery_errors = false
24 | config.action_mailer.delivery_method = :letter_opener
25 |
26 | Delayed::Worker.delay_jobs = false
27 | end
28 |
29 | require_relative '_shared'
30 |
--------------------------------------------------------------------------------
/config/initializers/whitelist_interceptor.rb:
--------------------------------------------------------------------------------
1 | class WhitelistInterceptor
2 | WHITELIST = DispatchConfiguration.staff_domains
3 |
4 | def self.delivering_email(message)
5 | original_to = message.to
6 | redirected = false
7 |
8 | message.to = Array(message.to).map do |address|
9 | whitelisted = false
10 |
11 | WhitelistInterceptor::WHITELIST.each do |whitelisted_address|
12 | break if whitelisted
13 | whitelisted = address.ends_with?("@#{whitelisted_address}")
14 | end
15 |
16 | if whitelisted
17 | address
18 | else
19 | redirected = true
20 | DispatchConfiguration.redirect_email_to
21 | end
22 | end
23 |
24 | message.subject = if redirected
25 | "(#{original_to} #{Rails.env}) " + message.subject
26 | else
27 | "(#{Rails.env}) " + message.subject
28 | end
29 | end
30 | end
31 |
32 | if Rails.env.staging?
33 | ActionMailer::Base.register_interceptor(WhitelistInterceptor)
34 | end
35 |
--------------------------------------------------------------------------------
/themes/dvl-core/assets/stylesheets/theme/components/filter_form.scss:
--------------------------------------------------------------------------------
1 | @import 'theme/includes.scss';
2 |
3 | .filter_clear {
4 | position: absolute;
5 | letter-spacing: $letterspaceSmallest;
6 | font-size: $fontSmallest;
7 | right: $rhythm * 2;
8 | top: $rhythm * 1.5;
9 | text-transform: uppercase;
10 | }
11 |
12 | .info_box_filters {
13 | display: none;
14 |
15 | &.is_active {
16 | display: block;
17 | }
18 |
19 | @media screen and (min-width: $lapWidth) {
20 | display: block;
21 | }
22 | }
23 |
24 | .filter_button_mobile {
25 | color: $darkerGray;
26 | background: transparent;
27 | font-size: $lineHeight;
28 | padding: $rhythm;
29 | border: 0;
30 | border-radius: $radius;
31 | &:focus,
32 | &:hover {
33 | outline: 0;
34 | background: rgba($primaryColor,0.25);
35 | color: $primaryColor;
36 | }
37 | span {
38 | @include hidden;
39 | }
40 | &.is_active {
41 | background: $primaryColor;
42 | color: $white;
43 | }
44 | @media screen and (min-width: $lapWidth) {
45 | display: none;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/lib/submission_adapters/base.rb:
--------------------------------------------------------------------------------
1 | module SubmissionAdapters
2 | class Base
3 | # The adapter's name. This will be displayed when posting an opportunity
4 | class_attribute :select_text
5 |
6 | # If true, we'll display a submission page and look for a template in ...
7 | class_attribute :submission_page
8 |
9 | def initialize(opportunity)
10 | @opportunity = opportunity
11 | end
12 |
13 | def view_proposals_url
14 | end
15 |
16 | def view_proposals_link_text
17 | end
18 |
19 | def submit_proposals_url
20 | end
21 |
22 | def submit_proposals_instructions
23 | end
24 |
25 | def submittable?
26 | submission_page ||
27 | submit_proposals_url ||
28 | submit_proposals_instructions
29 | end
30 |
31 | def self.to_adapter_name
32 | name.split('::').last
33 | end
34 |
35 | def self.to_param
36 | to_adapter_name.parameterize.underscore
37 | end
38 |
39 | def to_param
40 | self.class.to_param
41 | end
42 |
43 | def valid?
44 | true
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/app/controllers/attachments_controller.rb:
--------------------------------------------------------------------------------
1 | class AttachmentsController < ApplicationController
2 | before_action :set_opportunity
3 | before_action { authorize @opportunity, :edit? }
4 | before_action :set_attachment
5 |
6 | def create
7 | @attachment = @opportunity.attachments.build(upload: params[:file])
8 |
9 | if @attachment.save
10 | render json: {
11 | ok: true,
12 | html: render_to_string(
13 | partial: 'opportunities/edit_attachment',
14 | locals: { attachment: @attachment }
15 | )
16 | }
17 | else
18 | render json: {
19 | error: @attachments.errors.messages.join('. ')
20 | }, status: :bad_request
21 | end
22 | end
23 |
24 | def destroy
25 | @attachment.destroy
26 | render json: { ok: true }
27 | end
28 |
29 | private
30 |
31 | def set_opportunity
32 | @opportunity = Opportunity.find(params[:opportunity_id])
33 | end
34 |
35 | def set_attachment
36 | if params[:id]
37 | @attachment = @opportunity.attachments.find(params[:id])
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/spec/jobs/user_search_results_job_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe UserSearchResultsJob do
4 | let!(:user) { create(:user) }
5 | let!(:opportunity) { create(:opportunity, :approved) }
6 | let!(:not_approved_opportunity) { create(:opportunity) }
7 | let!(:saved_search) { create(:saved_search, user: user) }
8 | let!(:saved_search_two) { create(:saved_search, user: user) }
9 | let!(:saved_search_not_matching) do
10 | create(
11 | :saved_search,
12 | user: user,
13 | search_params: { 'text' => 'lolnomatch' }
14 | )
15 | end
16 |
17 | it 'notifies the user *once* of approved opportunities' do
18 | expect_any_instance_of(Mailer).to receive(:search_results).
19 | with(user, [opportunity.id]).and_call_original
20 |
21 | 2.times do
22 | described_class.perform_now(user)
23 | end
24 | end
25 |
26 | context 'without saved searches' do
27 | it 'does not send a mailer' do
28 | expect_any_instance_of(Mailer).to_not receive(:search_results)
29 | described_class.perform_now(create(:user))
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/config/environments/_shared.rb:
--------------------------------------------------------------------------------
1 | # This stuff needs to be loaded *after* the environment-specific config,
2 | # otherwise we'd just toss it in config/application.rb
3 | Rails.application.configure do
4 | # Check for DispatchConfiguration.asset_host, our Cloudfront URL
5 | if DispatchConfiguration.asset_host.present?
6 | config.action_controller.asset_host = DispatchConfiguration.asset_host
7 | end
8 |
9 | # Set "host with protocol", which is a nice variable to have
10 | host_with_protocol = "http#{DispatchConfiguration.ssl ? 's' : ''}://" +
11 | DispatchConfiguration.base_domain
12 |
13 | # Use the above variable to set some default URL options
14 | routes.default_url_options[:host] = host_with_protocol
15 | config.action_mailer.default_url_options = {
16 | host: host_with_protocol
17 | }
18 |
19 | # Set the asset host for our emails
20 | config.action_mailer.asset_host = config.action_controller.asset_host ||
21 | "//#{DispatchConfiguration.base_domain}"
22 |
23 | if DispatchConfiguration.ssl
24 | config.force_ssl = true
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/themes/dvl-core/assets/stylesheets/theme/sections/opportunities/edit.scss:
--------------------------------------------------------------------------------
1 | @import 'theme/includes';
2 |
3 | #opportunity_description {
4 | min-height: 14rem;
5 | }
6 |
7 | .opportunity_contact_name {
8 | margin-top: $rhythm;
9 | }
10 |
11 | .styled_select_wrapper + .microcopy,
12 | input[type=text] + .microcopy {
13 | margin-top: $rhythm;
14 | }
15 |
16 | .opportunity_rename_form {
17 | margin-bottom: $rhythm;
18 | div {
19 | display: table-cell;
20 | vertical-align: middle;
21 | }
22 | input {
23 | @include fontDisplay;
24 | font-size: $fontSizeH2;
25 | @include font_smoothing;
26 | height: $fontSizeH2 + ($inputPadding * 2);
27 | color: $black;
28 | }
29 | }
30 |
31 | .opportunity_rename_input {
32 | width: 100%;
33 | }
34 |
35 | .opportunity_rename_button {
36 | width: 1px;
37 | padding-left: $rhythm * 2;
38 | }
39 |
40 | .submission_adapter_error {
41 | margin-bottom: $rhythm * 2;
42 |
43 | @media screen and (min-width: $lapWidth) {
44 | margin-top: - ($rhythm * 2);
45 | padding-left: 25%;
46 | }
47 |
48 | .form_error {
49 | margin-top: 0;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/config/application.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../boot', __FILE__)
2 |
3 | require 'rails/all'
4 |
5 | # Require the gems listed in Gemfile, including any gems
6 | # you've limited to :test, :development, or :production.
7 | Bundler.require(:default, Rails.env)
8 |
9 | module Dispatch
10 | class Application < Rails::Application
11 | # Autoload /lib classes
12 | config.autoload_paths << Rails.root.join('lib')
13 |
14 | # Add fonts to asset pipeline
15 | config.assets.paths << Rails.root.join('app', 'assets', 'fonts')
16 |
17 | config.assets.precompile += %w(
18 | .svg .eot .woff .ttf
19 | mailer.css
20 | )
21 |
22 | # Dump as SQL since we use some Postgres-specific db features
23 | config.active_record.schema_format = :sql
24 |
25 | config.active_record.raise_in_transactional_callbacks = true
26 |
27 | config.action_dispatch.rescue_responses.merge!(
28 | 'ActionController::ParameterMissing' => :unprocessable_entity
29 | )
30 |
31 | config.active_job.queue_adapter = :delayed_job
32 |
33 | config.action_view.raise_on_missing_translations = true
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/themes/dvl-core/assets/stylesheets/theme/sections/index.scss:
--------------------------------------------------------------------------------
1 | @import 'theme/includes';
2 |
3 | body.index {
4 | .navbar {
5 | background: transparent;
6 | position: absolute;
7 | top: 0;
8 | left: 0;
9 | width: 100%;
10 | @include font_smoothing;
11 | .button {
12 | margin-left: $rhythm * 2;
13 | }
14 | }
15 | .navbar_content_wrapper {
16 | border-radius: $radius;
17 | background: $primaryColor;
18 | @media screen and (min-width: $deskWidth) {
19 | background: transparent;
20 | }
21 | }
22 | .navbar_content > ul > li > a {
23 | padding: $rhythm ($rhythm * 2);
24 | margin-top: $rhythm;
25 | @media screen and (min-width: $deskWidth) {
26 | line-height: $lineHeight;
27 | border-radius: $lineHeight;
28 | }
29 | }
30 | }
31 |
32 | .recent_opps {
33 | width: 90%;
34 | margin: 0 5%;
35 | @media screen and (min-width: $lapWidth) {
36 | width: 35rem;
37 | margin: 0 auto;
38 | }
39 | h2 {
40 | text-align: center;
41 | }
42 | }
43 |
44 |
45 | .recent_opps_more {
46 | text-align: center;
47 | margin-bottom: $rhythm * 5;
48 | }
49 |
--------------------------------------------------------------------------------
/app/helpers/formatted_timestamp_helper.rb:
--------------------------------------------------------------------------------
1 | module FormattedTimestampHelper
2 | def long_timestamp(time)
3 | formatted_timestamp(time, :long)
4 | end
5 |
6 | def short_timestamp(time)
7 | formatted_timestamp(time, :short)
8 | end
9 |
10 | def relative_timestamp(time)
11 | formatted_timestamp(time, :relative)
12 | end
13 |
14 | private
15 |
16 | def formatted_timestamp(time, format)
17 | # If we're in the current year, don't display the year
18 | if format == :long && time.year == Time.now.year
19 | format = :long_no_year
20 | end
21 |
22 | text_version = if format == :relative
23 | time_ago_in_words(time)
24 | else
25 | time.strftime(I18n.t("time.formats.#{format}"))
26 | end
27 |
28 | strftime_format = unless format == :relative
29 | I18n.t("time.formats.#{format}")
30 | end
31 |
32 | "" \
34 | "#{text_version} ".html_safe
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/app/controllers/users/registrations_controller.rb:
--------------------------------------------------------------------------------
1 | module Users
2 | class RegistrationsController < Devise::RegistrationsController
3 | before_filter :set_signup_type, only: [:new, :create]
4 | before_filter :set_edit_type, only: [:edit, :update]
5 |
6 | def confirm
7 | require_no_authentication
8 | end
9 |
10 | def update_resource(resource, params)
11 | if params.key?(:current_password)
12 | super
13 | else
14 | resource.update_without_password(params)
15 | end
16 | end
17 |
18 | def after_inactive_sign_up_path_for(_resource)
19 | flash.delete(:notice) # Don't display a redundant flash
20 | users_confirm_path
21 | end
22 |
23 | def after_update_path_for(_resource)
24 | edit_user_registration_path
25 | end
26 |
27 | def set_signup_type
28 | @signup_type = params[:type] == 'staff' ? :staff : :vendor
29 | end
30 |
31 | def set_edit_type
32 | @edit_type = params[:type] == 'password' ? :password : :account
33 | end
34 |
35 | def sign_up_params
36 | super.tap do |h|
37 | h[:signup_type] = @signup_type
38 | end
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | config.cache_store = :memory_store
3 | config.cache_classes = true
4 | config.eager_load = false
5 |
6 | # Configure static asset server for tests with Cache-Control for performance.
7 | config.serve_static_files = true
8 | config.static_cache_control = 'public, max-age=3600'
9 |
10 | # Show full error reports and disable caching.
11 | config.consider_all_requests_local = false
12 | config.action_controller.perform_caching = false
13 |
14 | # Raise exceptions instead of rendering exception templates.
15 | config.action_dispatch.show_exceptions = true
16 |
17 | # Disable request forgery protection in test environment.
18 | config.action_controller.allow_forgery_protection = false
19 |
20 | # Tell Action Mailer not to deliver emails to the real world.
21 | # The :test delivery method accumulates sent emails in the
22 | # ActionMailer::Base.deliveries array.
23 | config.action_mailer.delivery_method = :test
24 |
25 | # Print deprecation notices to the stderr.
26 | config.active_support.deprecation = :stderr
27 |
28 | Delayed::Worker.delay_jobs = false
29 | end
30 |
31 | require_relative '_shared'
32 |
--------------------------------------------------------------------------------
/spec/models/attachment_spec.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: attachments
4 | #
5 | # id :integer not null, primary key
6 | # opportunity_id :integer
7 | # upload :string
8 | # content_type :string
9 | # file_size_bytes :integer
10 | # has_thumbnail :boolean default(FALSE), not null
11 | # deleted_at :datetime
12 | # created_at :datetime not null
13 | # updated_at :datetime not null
14 | # text_content :text
15 | #
16 | # Indexes
17 | #
18 | # index_attachments_on_opportunity_id (opportunity_id)
19 | #
20 |
21 | require 'spec_helper'
22 |
23 | describe Attachment do
24 | subject { build(:attachment) }
25 | it { should be_valid }
26 |
27 | describe '#text_content' do
28 | its(:text_content) { should include 'hi!' }
29 |
30 | context 'when attachment is a docx' do
31 | subject { build(:attachment, :docx) }
32 | its(:text_content) { should include 'Hey there, this is a word doc!' }
33 | end
34 |
35 | context 'when attachment is an empty file' do
36 | subject { build(:attachment, :empty) }
37 | its(:text_content) { should be_blank }
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/themes/dvl-core/assets/stylesheets/theme/components/info_box.scss:
--------------------------------------------------------------------------------
1 | @import 'theme/includes';
2 |
3 | .info_box {
4 | border: 1px solid $gray;
5 | border-radius: $radius;
6 | box-shadow: 0 1px 0 rgba($black,0.06);
7 | margin-bottom: $lineHeight;
8 | h4 {
9 | margin: 0;
10 | font-weight: $weightNormal;
11 | }
12 | }
13 |
14 | .info_box_header {
15 | display: block;
16 | background: $lightestGray;
17 | @include border_top_radius($radius);
18 | position: relative;
19 | }
20 |
21 | // When header is a link...
22 | a.info_box_header {
23 | text-decoration: none;
24 |
25 | &:hover {
26 | background: $lighterGray;
27 | }
28 | }
29 |
30 | .info_box_header,
31 | .info_box_body {
32 | padding: $rhythm ($rhythm * 2);
33 | }
34 |
35 | .info_box_body {
36 | margin-top: $rhythm;
37 | font-size: $fontSmall;
38 | input {
39 | font-size: $fontSmall;
40 | }
41 | p:last-child,
42 | .input_group {
43 | margin-bottom: $rhythm;
44 | }
45 | }
46 |
47 | .info_box_contact {
48 | strong, span {
49 | display: block;
50 | color: $darkerGray;
51 | @include font_smoothing;
52 | }
53 | span {
54 | color: $darkerGray;
55 | margin-bottom: $rhythm / 2;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/app/views/users/registrations/_new_vendor.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for(:page_title, t('.title')) %>
2 | <% content_for(:main_container_class, 'container_tiny') %>
3 |
4 |
7 |
8 | <%= simple_form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
9 |
10 |
11 | <%= f.simple_fields_for :business_data, OpenStruct.new(f.object.business_data) do |f| %>
12 | <%= render partial: 'business_data', locals: { f: f } %>
13 | <% end %>
14 |
15 | <%= f.input :name, label: t('.name'), required: true %>
16 | <%= f.input :email, label: t('.email'), required: true %>
17 | <%= f.input :password, label: t('.password'), hint: t('password_hint'), required: true %>
18 | <%= f.input :business_name, label: t('.business_name.label'), hint: t('.business_name.hint') %>
19 |
20 | <%= f.input :subscribe_to_category_ids, label: t('.select_categories.label'), hint: t('.select_categories.hint'), as: :select, collection: Category.all, input_html: { multiple: true, 'data-no-styled-select' => true } %>
21 |
22 | <%= f.button :button, t('sign_up'), class: 'primary' %>
23 | <% end %>
24 |
--------------------------------------------------------------------------------
/config/environments/_shared_staging_production.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Rails-y configs
3 | config.cache_classes = true
4 | config.eager_load = true
5 | config.consider_all_requests_local = false
6 | config.action_controller.perform_caching = true
7 |
8 | # Nginx serves our assets
9 | config.serve_static_files = true
10 |
11 | # Compress JavaScripts and CSS.
12 | config.assets.js_compressor = :uglifier
13 | config.assets.css_compressor = :sass
14 |
15 | # Do not fallback to assets pipeline if a precompiled asset is missed.
16 | config.assets.compile = false
17 |
18 | # Generate digests for assets URLs.
19 | config.assets.digest = true
20 |
21 | # Version of your assets, change this if you want to expire all your assets.
22 | config.assets.version = '1.0'
23 |
24 | # Necessary for rails_oneline_logging
25 | config.log_level = :info
26 |
27 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
28 | # the I18n.default_locale when a translation can not be found).
29 | config.i18n.fallbacks = true
30 |
31 | # Send deprecation notices to registered listeners.
32 | config.active_support.deprecation = :notify
33 | end
34 |
35 | require_relative '_smtp_env_vars'
36 |
--------------------------------------------------------------------------------
/themes/dvl-core/assets/stylesheets/theme/components/opportunity_timeline.scss:
--------------------------------------------------------------------------------
1 | @import 'theme/includes';
2 |
3 | .opportunity_timeline {
4 | padding-left: $rhythm * 3;
5 | margin-bottom: $lineHeight;
6 |
7 | > li {
8 | font-size: $fontSmall;
9 | margin-bottom: $rhythm * 2;
10 | position: relative;
11 |
12 | &:before {
13 | content: '';
14 | position: absolute;
15 | top: $rhythm;
16 | left: - 2.5rem; // horizontally center on timeline
17 | border: 1px solid $gray;
18 | border-radius: 50%;
19 | width: $lineHeight;
20 | height: $lineHeight;
21 | background: #fff;
22 | z-index: 2;
23 | }
24 | &:after {
25 | position: absolute;
26 | content: '';
27 | left: $rhythm * -3.5;
28 | height: 100%;
29 | top: 75%;
30 | width: 1px;
31 | background-color: $darkGray;
32 | }
33 | &:first-child:after {
34 | top: 75%;
35 | height: 87.5%;
36 | }
37 | &:last-child:after {
38 | content: none;
39 | }
40 | &.is_past {
41 | opacity: 0.6;
42 | }
43 | }
44 | }
45 |
46 | .opportunity_timeline_event {
47 | font-weight: $weightBold;
48 | color: $darkerGray;
49 | font-size: $fontSmaller;
50 | }
51 |
--------------------------------------------------------------------------------
/app/views/users/sessions/new.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for(:page_title, t('sign_in')) %>
2 | <% content_for(:page_key, 'sign-in') %>
3 | <% content_for(:main_container_class, 'container_tiny') %>
4 |
5 |
8 |
9 |
10 | <%= t('sign_in_reg_hint_html', vendor_href: new_user_registration_path(type: 'vendor'), staff_href: new_user_registration_path(type: 'staff')) %>
11 |
12 |
13 | <%= simple_form_for resource, as: resource_name, url: session_path(resource_name) do |f| %>
14 | <%= f.input :email, required: false, autofocus: true %>
15 | <%= f.input :password, required: false , hint: link_to(t('forgot'), new_password_path(resource_name)).html_safe
16 | %>
17 |
18 | Keep me logged in for two weeks
19 |
20 | <%= f.button :submit, t('sign_in'), class: 'primary' %>
21 | <% end %>
22 |
23 | <% if devise_mapping.confirmable? %>
24 |
25 | <%= link_to "Resend confirmation email", new_confirmation_path(resource_name) %>
26 |
27 | <% end %>
28 |
--------------------------------------------------------------------------------
/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= calculated_page_title %>
5 |
6 | <%= stylesheet_link_tag('application', media: 'all') %>
7 | <%= render 'webfonts' %>
8 | <%= yield(:extra_css) %>
9 |
10 |
11 | <%= csrf_meta_tags %>
12 | <% unless Rails.env.test? %>
13 |
14 | <% end %>
15 |
16 |
17 |
18 | <%= render('navbar') %>
19 | <%= render('flashes') %>
20 | <%= render('inner_layout') %>
21 | <%= render('footer') %>
22 | <%= javascript_include_tag('application') %>
23 | <%= yield(:extra_js) %>
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/models/question.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: questions
4 | #
5 | # id :integer not null, primary key
6 | # opportunity_id :integer
7 | # asked_by_user_id :integer
8 | # answered_by_user_id :integer
9 | # question_text :text
10 | # answer_text :text
11 | # answered_at :datetime
12 | # deleted_at :datetime
13 | # created_at :datetime not null
14 | # updated_at :datetime not null
15 | #
16 | # Indexes
17 | #
18 | # index_questions_on_opportunity_id (opportunity_id)
19 | #
20 |
21 | class Question < ActiveRecord::Base
22 | has_storage_unit
23 |
24 | belongs_to :opportunity
25 | belongs_to :asked_by_user, class_name: 'User'
26 | belongs_to :answered_by_user, class_name: 'User'
27 |
28 | scope :unanswered, -> { where('answered_at IS NULL') }
29 | scope :answered, -> { where('answered_at IS NOT NULL') }
30 |
31 | scope :unanswered_first, -> {
32 | order('answered_at DESC NULLS FIRST, created_at ASC')
33 | }
34 |
35 | validates :opportunity, presence: true
36 | validates :asked_by_user, presence: true
37 | validates :question_text, presence: true
38 |
39 | def answered?
40 | answered_at.present?
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | root to: 'home#index'
3 |
4 | namespace :admin do
5 | resources :users
6 | resources :attachments
7 | resources :categories
8 | resources :departments
9 | resources :opportunities
10 | resources :questions
11 | resources :saved_searches
12 |
13 | root to: 'users#index'
14 | end
15 |
16 | devise_for :users, controllers: {
17 | sessions: 'users/sessions',
18 | passwords: 'users/passwords',
19 | registrations: 'users/registrations',
20 | confirmations: 'users/confirmations'
21 | }
22 |
23 | devise_scope :user do
24 | get 'users/confirm' => 'users/registrations#confirm'
25 | end
26 |
27 | DispatchConfiguration.static_pages.each do |x|
28 | get x['path'] => "static##{x['path']}"
29 | end
30 |
31 | resources :saved_searches, only: [:create, :destroy]
32 |
33 | resources :opportunities do
34 | collection do
35 | get 'feed'
36 | get 'pending'
37 | end
38 |
39 | member do
40 | get 'submit'
41 | post 'approve'
42 | post 'subscribe'
43 | post 'request_approval'
44 | end
45 |
46 | resources :questions, only: [:create, :update, :destroy]
47 | resources :attachments, only: [:create, :destroy]
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/app/filterers/opportunity_filterer.rb:
--------------------------------------------------------------------------------
1 | class OpportunityFilterer < Filterer::Base
2 | sort_option 'title', 'LOWER(title)', default: true
3 | sort_option 'department', 'LOWER(opp_department.name)'
4 | sort_option 'submissions_close_at'
5 | sort_option 'updated_at'
6 |
7 | def param_text(x)
8 | results.full_text(x)
9 | end
10 |
11 | def param_status(x)
12 | if x == 'open'
13 | results.submissions_open
14 | elsif x == 'closed'
15 | results.submissions_closed
16 | else
17 | results
18 | end
19 | end
20 |
21 | def param_category_ids(x)
22 | category_ids = Array(x).
23 | select { |category_id| category_id.to_s =~ /\A[0-9]+\Z/ }.
24 | compact
25 |
26 | if category_ids.present?
27 | results.with_any_category(category_ids)
28 | else
29 | results
30 | end
31 | end
32 |
33 | def param_department_id(x)
34 | results.where(department_id: x)
35 | end
36 |
37 | def defaults
38 | {
39 | status: 'open'
40 | }
41 | end
42 |
43 | def apply_default_filters
44 | results.
45 | # Workaround for pg_search bullshit
46 | joins('LEFT JOIN departments as opp_department ON opp_department.id = ' \
47 | 'opportunities.department_id')
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/app/views/users/registrations/_edit_account.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for(:page_title, 'Your account') %>
2 | <% content_for(:main_container_class, 'container_tiny') %>
3 |
4 |
7 |
8 | <%= simple_form_for resource, as: resource_name, url: registration_path(resource_name), html: { method: :put } do |f| %>
9 |
10 |
11 | <% unless current_user.permission_level_is_at_least?('staff') %>
12 | <%= f.input :business_name, label: t('business_name'), required: true %>
13 |
14 | <%= f.simple_fields_for :business_data, OpenStruct.new(f.object.business_data) do |f| %>
15 | <%= render partial: 'business_data', locals: { f: f } %>
16 | <% end %>
17 | <% end %>
18 |
19 | <%= f.input :name, label: t('full_name') %>
20 | <%= f.input :email, required: true %>
21 |
22 | <%= f.input :password, hint: false do %>
23 | ••••••••
24 | ' class='button mini'>Change
25 | <% end %>
26 |
27 | <%= f.button :button, t('save_changes'), class: 'primary' %>
28 | <% end %>
29 |
30 | <%= render 'saved_searches' %>
31 | <%= render 'my_opportunities' %>
32 |
--------------------------------------------------------------------------------
/lib/tasks/db.rake:
--------------------------------------------------------------------------------
1 | namespace :db do
2 | namespace :seed do
3 | desc 'seed example data'
4 | task example: :environment do
5 | user = FactoryGirl.create(
6 | :user,
7 | name: 'Demo User',
8 | email: 'admin@example.com',
9 | permission_level: 'admin',
10 | confirmed_at: Time.now
11 | )
12 |
13 | departments = Array(3).map { FactoryGirl.create(:department) }
14 |
15 | categories = Array(10).map { FactoryGirl.create(:category) }
16 |
17 | opp = FactoryGirl.create(
18 | :opportunity,
19 | :published,
20 | :approved,
21 | created_by_user: user,
22 | department: departments.sample,
23 | submissions_open_at: Time.now + 1.day,
24 | submissions_close_at: Time.now + 1.month
25 | )
26 |
27 | opp.categories = categories.sample(2)
28 |
29 | opp_two = FactoryGirl.create(
30 | :opportunity,
31 | :published,
32 | created_by_user: user,
33 | department: departments.sample
34 | )
35 |
36 | opp_two.categories << categories.sample
37 | end
38 | end
39 |
40 | all = [:environment, :drop, :create, 'structure:load', :seed, 'seed:example']
41 |
42 | task all: all
43 | task all_via_migration: all.map! { |x| x == 'structure:load' ? :migrate : x }
44 | end
45 |
--------------------------------------------------------------------------------
/spec/features/opportunities/pending_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'Opportunities - Pending' do
4 | let!(:user) { create(:user) }
5 |
6 | let!(:pending_publish_opportunity) do
7 | create(:opportunity, :approved, publish_at: Time.now + 1.hour)
8 | end
9 |
10 | let!(:pending_approval_opportunity) { create(:opportunity) }
11 | let(:published_opportunity) { create(:opportunity, :approved) }
12 |
13 | before { login_as user }
14 |
15 | context 'as a vendor' do
16 | it 'denies access' do
17 | visit pending_opportunities_path
18 | expect(current_path).to eq root_path
19 | end
20 | end
21 |
22 | context 'as a staff member' do
23 | before do
24 | user.staff!
25 | pending_publish_opportunity.update(created_by_user: user)
26 | end
27 |
28 | it 'denies access' do
29 | visit pending_opportunities_path
30 | expect(current_path).to eq root_path
31 | end
32 | end
33 |
34 | context 'as an approver' do
35 | before { user.approver! }
36 |
37 | it 'renders all pending opportunities' do
38 | visit pending_opportunities_path
39 | expect(page).to have_text pending_publish_opportunity.title
40 | expect(page).to have_text pending_approval_opportunity.title
41 | expect(page).to_not have_text published_opportunity.title
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/app/jobs/user_search_results_job.rb:
--------------------------------------------------------------------------------
1 | # Run on an interval. Will look for responses that match *any* of a
2 | # user's saved searches.
3 | class UserSearchResultsJob < ApplicationJob
4 | def perform(user)
5 | opp_ids = user.
6 | saved_searches.
7 | map { |saved_search| saved_search_opp_ids(saved_search) }.
8 | flatten.
9 | uniq
10 |
11 | notify_opp_ids = opp_ids - already_notified_of_opp_ids(user)
12 | return unless notify_opp_ids.present?
13 | Mailer.search_results(user, notify_opp_ids).deliver_later
14 | create_audit(user, notify_opp_ids)
15 | end
16 |
17 | private
18 |
19 | def create_audit(user, notify_opp_ids)
20 | user.audits.create(
21 | event: audit_event,
22 | data: { 'opportunity_ids' => notify_opp_ids }
23 | )
24 | end
25 |
26 | def already_notified_of_opp_ids(user)
27 | user.
28 | audits.
29 | where(event: audit_event).
30 | pluck(:data).
31 | map { |data| Array(data['opportunity_ids']) }.
32 | flatten.
33 | uniq
34 | end
35 |
36 | def saved_search_opp_ids(saved_search)
37 | Opportunity.
38 | posted.
39 | filter(saved_search.search_params, skip_pagination: true).
40 | pluck(:id)
41 | end
42 |
43 | def audit_event
44 | 'user_search_results.notify'
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/docs/marketing.md:
--------------------------------------------------------------------------------
1 | Dispatch
2 | ---
3 |
4 | ## Easy vendor signup
5 |
6 | Attracting small businesses to government contracting is hard. Don't make it harder by making them navigate an arcane registration form in order to see contracting opportunities.
7 |
8 | ## Email alerts
9 |
10 | Vendors will automatically receive alerts of new opportunities that match their capabilities.
11 |
12 | ## Powerful search
13 |
14 | Dispatch indexes the contents of your RFP documents so that vendors can easily find opportunities that they're interested in.
15 |
16 | ## Online proposal review
17 |
18 | [Insert Screendoor copy here]
19 |
20 | ## Open-data friendly
21 |
22 | [Insert Screendoor copy here]
23 |
24 | ## Customizable look-and-feel
25 |
26 | Dispatch can be configured to match the colors of your existing website. It can even be extended to use an in-house style guide like the [US Web Design Standards](https://pages.18f.gov/designstandards/).
27 |
28 | ## Open source
29 |
30 | Dispatch is free and open-source software, meaning that you'll never pay a cent in license costs. The fact that Dispatch is open-source also means that you don't have to take our word with regard to its quality: you can see for yourself that Dispatch's code is clean, well-tested, and secure.
31 |
32 | Hosting, customization, and service level agreements (SLAs) are available from the Department of Better Technology.
33 |
--------------------------------------------------------------------------------
/app/views/opportunities/pending.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for(:page_key, 'opportunities-pending') %>
2 | <% content_for(:page_title, t('approve')) %>
3 | <% content_for(:main_container_class, 'container_tiny') %>
4 |
5 | <% if @pending_approval_opportunities.present? %>
6 |
9 |
10 | <%= t('.pending_approval.hint') %>
11 |
12 |
13 | <% @pending_approval_opportunities.each do |opportunity| %>
14 | <%= render partial: 'pending_item', locals: { opportunity: opportunity } %>
15 | <% end %>
16 |
17 | <% else %>
18 |
19 |
20 |
<%= t('.pending_approval.none.title') %>
21 | <%= t('.pending_approval.none.hint') %>
22 |
23 | <% end %>
24 |
25 | <% if @pending_publish_opportunities.present? %>
26 |
29 |
30 |
31 | <% @pending_publish_opportunities.each do |opportunity| %>
32 | <%= render partial: 'pending_item', locals: { opportunity: opportunity } %>
33 | <% end %>
34 |
35 | <% end %>
36 |
--------------------------------------------------------------------------------
/app/policies/opportunity_policy.rb:
--------------------------------------------------------------------------------
1 | class OpportunityPolicy < Struct.new(:user, :opportunity)
2 | def show?
3 | opportunity.posted? ||
4 | opportunity_admin?
5 | end
6 |
7 | def edit?
8 | opportunity_admin?
9 | end
10 |
11 | def destroy?
12 | user && user.permission_level_is_at_least?('admin')
13 | end
14 |
15 | def create?
16 | user && user.permission_level_is_at_least?('staff')
17 | end
18 |
19 | def approve?
20 | user && user.permission_level_is_at_least?('approver')
21 | end
22 |
23 | def submit?
24 | # Allow admins to see what their submission form will look like
25 | opportunity.submittable? &&
26 | (opportunity_admin? || opportunity.open_for_submissions?)
27 | end
28 |
29 | def ask_question?
30 | user &&
31 | opportunity.open_for_questions?
32 | end
33 |
34 | def answer_questions?
35 | opportunity.enable_questions? &&
36 | opportunity_admin?
37 | end
38 |
39 | def subscribe?
40 | show? &&
41 | user
42 | end
43 |
44 | def review_submissions?
45 | opportunity_admin? &&
46 | (
47 | opportunity.view_proposals_url ||
48 | opportunity.view_proposals_link_text
49 | )
50 | end
51 |
52 | private
53 |
54 | def opportunity_admin?
55 | user.try(:approver?) ||
56 | user.try(:admin?) ||
57 | (user.try(:staff?) && opportunity.created_by_user == user)
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/db/migrate/20160418203004_create_delayed_jobs.rb:
--------------------------------------------------------------------------------
1 | class CreateDelayedJobs < ActiveRecord::Migration
2 | def self.up
3 | create_table :delayed_jobs, force: true do |table|
4 | table.integer :priority, default: 0, null: false # Allows some jobs to jump to the front of the queue
5 | table.integer :attempts, default: 0, null: false # Provides for retries, but still fail eventually.
6 | table.text :handler, null: false # YAML-encoded string of the object that will do work
7 | table.text :last_error # reason for last failure (See Note below)
8 | table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future.
9 | table.datetime :locked_at # Set when a client is working on this object
10 | table.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead)
11 | table.string :locked_by # Who is working on this object (if locked)
12 | table.string :queue # The name of the queue this job is in
13 | table.timestamps null: true
14 | end
15 |
16 | add_index :delayed_jobs, [:priority, :run_at], name: 'delayed_jobs_priority'
17 | end
18 |
19 | def self.down
20 | drop_table :delayed_jobs
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/app/dashboards/saved_search_dashboard.rb:
--------------------------------------------------------------------------------
1 | require 'administrate/base_dashboard'
2 |
3 | class SavedSearchDashboard < Administrate::BaseDashboard
4 | # ATTRIBUTE_TYPES
5 | # a hash that describes the type of each of the model's fields.
6 | #
7 | # Each different type represents an Administrate::Field object,
8 | # which determines how the attribute is displayed
9 | # on pages throughout the dashboard.
10 | ATTRIBUTE_TYPES = {
11 | id: Field::Number,
12 | user: Field::BelongsTo,
13 | search_params: Field::Text,
14 | created_at: Field::DateTime,
15 | updated_at: Field::DateTime
16 | }.freeze
17 |
18 | # COLLECTION_ATTRIBUTES
19 | # an array of attributes that will be displayed on the model's index page.
20 | #
21 | # By default, it's limited to four items to reduce clutter on index pages.
22 | # Feel free to add, remove, or rearrange items.
23 | COLLECTION_ATTRIBUTES = [
24 | :id,
25 | :user,
26 | :search_params
27 | ].freeze
28 |
29 | # SHOW_PAGE_ATTRIBUTES
30 | # an array of attributes that will be displayed on the model's show page.
31 | SHOW_PAGE_ATTRIBUTES = [
32 | :id,
33 | :user,
34 | :search_params,
35 | :created_at,
36 | :updated_at
37 | ].freeze
38 |
39 | # FORM_ATTRIBUTES
40 | # an array of attributes that will be displayed
41 | # on the model's form (`new` and `edit`) pages.
42 | FORM_ATTRIBUTES = [
43 | :user
44 | ].freeze
45 | end
46 |
--------------------------------------------------------------------------------
/spec/features/admin/base_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'Admin' do
4 | context 'when user is an admin' do
5 | let!(:user) { create(:user, :admin) }
6 | before { login_as user }
7 |
8 | it 'renders properly' do
9 | visit admin_root_path
10 | expect(page).to have_selector 'h1', text: 'Users'
11 | end
12 |
13 | it 'visits each page' do
14 | visit admin_root_path
15 |
16 | links = all('.sidebar__link')[1..-1].map(&:text)
17 |
18 | links.each do |link|
19 | find('.sidebar__link', text: link).click
20 | end
21 | end
22 |
23 | context 'with deleted resources' do
24 | let!(:deleted_opportunity) { create(:opportunity, deleted_at: Time.now) }
25 |
26 | it 'shows resources' do
27 | visit admin_opportunities_path
28 | find('tr a', text: 'Edit').click
29 | expect(page).to have_text deleted_opportunity.title
30 | end
31 | end
32 |
33 | context 'with non soft-deleteable resources' do
34 | it 'shows resources' do
35 | visit admin_user_path(user)
36 | expect(page).to have_text user.name
37 | end
38 | end
39 | end
40 |
41 | context 'when user is not an admin' do
42 | let!(:user) { create(:user) }
43 | before { login_as user }
44 |
45 | it 'denies access' do
46 | visit admin_root_path
47 | expect(page).to have_text 'not authorized'
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/spec/jobs/send_deadline_reminders_job_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe SendDeadlineRemindersJob do
4 | let!(:user) { create(:user) }
5 | let!(:opportunity) do
6 | create(
7 | :opportunity,
8 | :approved,
9 | enable_questions: true,
10 | questions_close_at:
11 | Time.now +
12 | DispatchConfiguration.question_deadline_reminder_hours.hours -
13 | 20.minutes,
14 | submissions_close_at:
15 | Time.now +
16 | DispatchConfiguration.submission_deadline_reminder_hours.hours -
17 | 20.minutes
18 | )
19 | end
20 |
21 | context 'when the user is not subscribed' do
22 | it 'does not send notifications' do
23 | expect { described_class.perform_now }.
24 | to_not change { ActionMailer::Base.deliveries.length }
25 | end
26 | end
27 |
28 | context 'when the user is subscribed' do
29 | before { user.opportunities << opportunity }
30 |
31 | it 'sends notifications' do
32 | expect { described_class.perform_now }.
33 | to change { ActionMailer::Base.deliveries.length }.by(2)
34 | end
35 |
36 | context 'when a notification is already sent' do
37 | before { opportunity.update(submission_deadline_reminder_sent: true) }
38 |
39 | it 'only sends new notifications' do
40 | expect { described_class.perform_now }.
41 | to change { ActionMailer::Base.deliveries.length }.by(1)
42 | end
43 | end
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/app/helpers/opportunities_helper.rb:
--------------------------------------------------------------------------------
1 | module OpportunitiesHelper
2 | def filtered?
3 | @opportunities.filterer.params[:text].present? ||
4 | @opportunities.filterer.params[:status] != 'open' ||
5 | @opportunities.filterer.params[:category_ids].present? ||
6 | @opportunities.filterer.params[:department_id].present?
7 | end
8 |
9 | def existing_saved_search
10 | current_user &&
11 | filtered? &&
12 | current_user.saved_searches.detect do |saved_search|
13 | normalize_search_params(saved_search.search_params) ==
14 | normalize_search_params(current_filter_params)
15 | end
16 | end
17 |
18 | def normalize_search_params(search_params)
19 | search_params.stringify_keys.select do |_, v|
20 | v.present?
21 | end
22 | end
23 |
24 | def current_filter_params
25 | pick(@opportunities.filterer.params, *SavedSearch::PERMITTED_SEARCH_PARAMS)
26 | end
27 |
28 | def edit_opportunity_steps
29 | %w(
30 | description
31 | contact
32 | submissions
33 | )
34 | end
35 |
36 | def submission_adapter_edit_partial(adapter_class)
37 | "submission_adapters/#{adapter_class.to_param}/edit"
38 | end
39 |
40 | def opportunity_timeline_events
41 | pick(
42 | @opportunity,
43 | :publish_at,
44 | :submissions_open_at,
45 | :submissions_close_at,
46 | :questions_open_at,
47 | :questions_close_at
48 | ).select { |_, v| v.present? }.
49 | sort_by(&:last)
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/app/views/opportunities/edit/_description.html.erb:
--------------------------------------------------------------------------------
1 | <%= f.input :department_id, as: :select, collection: Department.all, include_blank: t('choose_a_department') %>
2 | <%= f.input :description %>
3 |
4 | <%= f.input :publish_at, as: :datetime_picker %>
5 | <%= f.input :category_ids, as: :select, collection: Category.all, input_html: { multiple: true, 'data-no-styled-select' => true } %>
6 |
7 | <%= f.input :enable_questions, as: :boolean, label: false, inline_label: t('allow_vendors_to_ask_questions') %>
8 |
9 | style='display:none'<% end %>>
10 | <%= f.input :question_dates, wrapper_html: { class: 'date_range' } do %>
11 | <%= f.input_field :questions_open_at, as: :datetime_picker %>
12 | and
13 | <%= f.input_field :questions_close_at, as: :datetime_picker %>
14 | <% end %>
15 |
16 |
17 | <%= f.input :attachments do %>
18 |
19 | <% @opportunity.attachments.each do |attachment| %>
20 | <%= render partial: 'edit_attachment', locals: { attachment: attachment } %>
21 | <% end %>
22 |
23 |
24 |
25 | Upload new attachment
26 |
27 |
28 | <% end %>
29 |
--------------------------------------------------------------------------------
/spec/mailers/previews/mailer_preview.rb:
--------------------------------------------------------------------------------
1 | class MailerPreview < ActionMailer::Preview
2 | def search_results
3 | Mailer.search_results(
4 | User.first,
5 | Opportunity.limit(3)
6 | )
7 | end
8 |
9 | def question_asked
10 | question = Question.first || FactoryGirl.create(:question)
11 |
12 | Mailer.question_asked(
13 | question.opportunity.created_by_user,
14 | question
15 | )
16 | end
17 |
18 | def question_answered
19 | question = Question.answered.first ||
20 | FactoryGirl.create(
21 | :question,
22 | :answered,
23 | opportunity: Opportunity.first,
24 | asked_by_user: User.first,
25 | answered_by_user: User.first
26 | )
27 |
28 | Mailer.question_answered(
29 | question.asked_by_user,
30 | question
31 | )
32 | end
33 |
34 | def approval_request
35 | Mailer.approval_request(
36 | User.first,
37 | Opportunity.first
38 | )
39 | end
40 |
41 | def question_deadline
42 | opp = Opportunity.first
43 | opp.questions_close_at = Time.now + 1.day + 12.hours
44 |
45 | Mailer.question_deadline(
46 | User.first,
47 | opp
48 | )
49 | end
50 |
51 | def submission_deadline
52 | opp = Opportunity.first
53 | opp.submissions_close_at = Time.now + 1.day + 12.hours
54 |
55 | Mailer.submission_deadline(
56 | User.first,
57 | opp
58 | )
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/app/controllers/admin/application_controller.rb:
--------------------------------------------------------------------------------
1 | # All Administrate controllers inherit from this `Admin::ApplicationController`,
2 | # making it the ideal place to put authentication logic or other
3 | # before_filters.
4 | #
5 | # If you want to add pagination or other controller-level concerns,
6 | # you're free to overwrite the RESTful controller actions.
7 | module Admin
8 | class ApplicationController < Administrate::ApplicationController
9 | before_filter :authenticate_admin
10 |
11 | def index
12 | search_term = params[:search].to_s.strip
13 | resources = Administrate::Search.new(resource_resolver, search_term).run
14 |
15 | # Allow deleted
16 | resources = resources.with_deleted if resources.respond_to?(:with_deleted)
17 |
18 | resources = order.apply(resources)
19 | resources = resources.page(params[:page]).per(records_per_page)
20 | page = Administrate::Page::Collection.new(dashboard, order: order)
21 |
22 | render locals: {
23 | resources: resources,
24 | search_term: search_term,
25 | page: page
26 | }
27 | end
28 |
29 | # Allow deleted
30 | def find_resource(param)
31 | if resource_class.respond_to?(:with_deleted)
32 | resource_class.with_deleted.find(param)
33 | else
34 | resource_class.find(param)
35 | end
36 | end
37 |
38 | def authenticate_admin
39 | unless current_user.try(:admin?)
40 | render text: 'not authorized', status: :unauthorized
41 | end
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/app/views/opportunities/_questions.erb:
--------------------------------------------------------------------------------
1 | <% if @questions.present? %>
2 |
3 |
6 |
7 | <% unanswered_questions = @questions.reject(&:answered?) %>
8 | <% if unanswered_questions.present? %>
9 |
10 | <%= t('unanswered_alert', count: unanswered_questions.length) %>
11 | <% @questions.reject(&:answered?).each do |question| %>
12 | <%= render question %>
13 | <% end %>
14 |
15 | <% end %>
16 |
17 | <% @questions.select(&:answered?).each do |question| %>
18 | <%= render question %>
19 | <% end %>
20 |
21 |
22 | <% end %>
23 |
24 | <% if policy(@opportunity).ask_question? %>
25 |
26 |
34 | <%= simple_form_for [@opportunity, @opportunity.questions.build] do |f| %>
35 |
36 | <%= f.input_field :question_text, 'aria-label' => t('question_text') %>
37 |
38 | <%= f.button :button, t('ask'), class: 'primary' %>
39 | <% end %>
40 |
41 | <% end %>
42 |
--------------------------------------------------------------------------------
/spec/features/opportunities/index_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'Opportunities - Index' do
4 | let!(:vendor) { create(:user) }
5 | let!(:opportunity) do
6 | create(:opportunity, :approved)
7 | end
8 |
9 | it 'shows the list of opportunities' do
10 | visit opportunities_path
11 | expect(page).to have_link opportunity.title
12 | end
13 |
14 | it 'filters properly' do
15 | visit opportunities_path
16 | expect(page).to have_text opportunity.title
17 | fill_in :opportunity_filters_text, with: 'nothingmatchesthis'
18 | find('.opportunity_filters button').click
19 | expect(page).to_not have_text opportunity.title
20 | expect(page).to have_link t('email_me'), href: new_user_session_path
21 |
22 | # Clear filters
23 | click_link t('clear')
24 | expect(page).to have_text opportunity.title
25 | end
26 |
27 | context 'when signed in' do
28 | before { login_as vendor }
29 |
30 | it 'saves a search' do
31 | visit opportunities_path
32 |
33 | # No links without filter
34 | expect(page).to_not have_link t('unsubscribe_from_search')
35 | expect(page).to_not have_link t('email_me')
36 |
37 | fill_in :opportunity_filters_text, with: 'nothingmatchesthis'
38 | find('.opportunity_filters button').click
39 | expect { click_link t('email_me') }.
40 | to change { vendor.saved_searches.count }.by(1)
41 |
42 | expect { click_link t('unsubscribe_from_search') }.
43 | to change { vendor.saved_searches.count }.by(-1)
44 | end
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | def calculated_page_title
3 | if content_for(:page_title).present?
4 | "#{content_for(:page_title)} - #{DispatchConfiguration.site_title}"
5 | else
6 | DispatchConfiguration.site_title
7 | end
8 | end
9 |
10 | # Workaround for https://github.com/plataformatec/devise/issues/3748
11 | def flashes_with_consistent_keys
12 | devise_flash_key_mappings = {
13 | 'notice' => 'success',
14 | 'alert' => 'error'
15 | }
16 |
17 | flash.
18 | to_h.
19 | map { |k, v| { (devise_flash_key_mappings[k.to_s] || k) => v } }.
20 | reduce(&:merge) || {}
21 | end
22 |
23 | def sortable_table_header(objects, key, name)
24 | filterer = objects.filterer
25 |
26 | content_tag(
27 | :a,
28 | href: url_for(
29 | params.merge(
30 | sort: key,
31 | page: nil,
32 | direction: if filterer.sort == key && filterer.direction == 'asc'
33 | 'desc'
34 | else
35 | 'asc'
36 | end
37 | )
38 | )
39 | ) do
40 | (name +
41 | (if filterer.sort == key
42 | ' '.html_safe +
43 | tag(
44 | :i,
45 | class: if filterer.direction == 'asc'
46 | 'fa fa-caret-up'
47 | else
48 | 'fa fa-caret-down'
49 | end
50 | )
51 | else
52 | ''
53 | end)).html_safe
54 | end
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/themes/dvl-core/assets/stylesheets/theme/components/account_saved_searches.scss:
--------------------------------------------------------------------------------
1 | @import 'theme/includes';
2 |
3 | .saved_filters li {
4 | position: relative;
5 | margin-bottom: $rhythm * 5;
6 | }
7 |
8 | .saved_filter_view {
9 | display: block;
10 | width: 100%;
11 | position: relative;
12 | border: 1px solid $gray;
13 | border-radius: $radius;
14 | box-shadow: 0 1px 0 rgba(#000,0.06);
15 | padding: $rhythm ($rhythm * 2) $rhythm ($rhythm * 8);
16 | transition: background-color 0.15s ease-out;
17 |
18 | &:hover,
19 | &:focus,
20 | &:active {
21 | text-decoration: none;
22 | outline: 0;
23 | }
24 | &:hover,
25 | &:focus {
26 | background: $lighterGray;
27 | }
28 |
29 | &:before {
30 | position: absolute;
31 | font-family: 'FontAwesome';
32 | content: '\f002';
33 | color: $darkGray;
34 | font-size: $rhythm * 4;
35 | top: $rhythm;
36 | left: $rhythm * 1.5;
37 | }
38 | span,
39 | em {
40 | display: block;
41 | @include font_smoothing;
42 | }
43 | span {
44 | font-size: 1.2rem;
45 | color: $darkestGray;
46 | }
47 | em {
48 | font-style: normal;
49 | font-size: $fontSmall;
50 | color: $darkerGray;
51 | }
52 | }
53 |
54 | .saved_filter_actions {
55 | position: absolute;
56 | font-size: $fontSmall;
57 | position: absolute;
58 | right: 0;
59 | top: 100%;
60 | margin-top: $rhythm;
61 | }
62 |
63 | @media screen and (min-width: $lapWidth) {
64 | .saved_filter_view {
65 | width: 80%;
66 | }
67 | .saved_filter_actions {
68 | top: 0;
69 | margin-top: 0;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/spec/features/opportunities/create_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'Opportunity creation' do
4 | include OpportunitiesHelper
5 |
6 | let!(:staff) { create(:user, :staff) }
7 | let!(:opportunity) { create(:opportunity, created_by_user: staff) }
8 | before { login_as staff }
9 |
10 | it 'renders each step sequentially' do
11 | visit new_opportunity_path
12 | fill_in :opportunity_title, with: 'newopp'
13 | find('#new_opportunity button').click
14 |
15 | edit_opportunity_steps.each do |x|
16 | click_link t("edit_opportunity_steps.#{x}.title")
17 | end
18 |
19 | find('.edit_opportunity button').click
20 | expect(page).to have_text 'newopp'
21 | end
22 |
23 | it 'renames the opportunity', js: true do
24 | visit new_opportunity_path
25 | fill_in :opportunity_title, with: 'newopp'
26 | find('#new_opportunity button').click
27 |
28 | find('[data-toggle-visible="[data-toggle-opportunity-rename]"]').click
29 | fill_in :opportunity_title, with: 'newname'
30 | click_button t('rename')
31 | expect(page).to have_text 'newname'
32 | expect(page).to_not have_text 'newopp'
33 | end
34 |
35 | it 'allows for uploading and destroying attachments', js: true do
36 | visit edit_opportunity_path(opportunity, step: 'description')
37 | show_hidden_file_inputs
38 | attach_file :attachment_upload,
39 | Rails.root.join('spec/fixtures/files/test.txt')
40 | wait_for_ajax
41 | expect(page).to have_text 'test.txt'
42 | find('.js-remove-attachment').click
43 | expect(page).to_not have_text 'test.txt'
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/app/dashboards/category_dashboard.rb:
--------------------------------------------------------------------------------
1 | require 'administrate/base_dashboard'
2 |
3 | class CategoryDashboard < Administrate::BaseDashboard
4 | # ATTRIBUTE_TYPES
5 | # a hash that describes the type of each of the model's fields.
6 | #
7 | # Each different type represents an Administrate::Field object,
8 | # which determines how the attribute is displayed
9 | # on pages throughout the dashboard.
10 | ATTRIBUTE_TYPES = {
11 | opportunities: Field::HasMany,
12 | id: Field::Number,
13 | name: Field::String,
14 | created_at: Field::DateTime,
15 | updated_at: Field::DateTime
16 | }.freeze
17 |
18 | # COLLECTION_ATTRIBUTES
19 | # an array of attributes that will be displayed on the model's index page.
20 | #
21 | # By default, it's limited to four items to reduce clutter on index pages.
22 | # Feel free to add, remove, or rearrange items.
23 | COLLECTION_ATTRIBUTES = [
24 | :opportunities,
25 | :id,
26 | :name
27 | ].freeze
28 |
29 | # SHOW_PAGE_ATTRIBUTES
30 | # an array of attributes that will be displayed on the model's show page.
31 | SHOW_PAGE_ATTRIBUTES = [
32 | :opportunities,
33 | :id,
34 | :name,
35 | :created_at,
36 | :updated_at
37 | ].freeze
38 |
39 | # FORM_ATTRIBUTES
40 | # an array of attributes that will be displayed
41 | # on the model's form (`new` and `edit`) pages.
42 | FORM_ATTRIBUTES = [
43 | :opportunities,
44 | :name
45 | ].freeze
46 |
47 | # Overwrite this method to customize how categories are displayed
48 | # across all pages of the admin dashboard.
49 | #
50 | def display_resource(category)
51 | "#{category.name} (##{category.id})"
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/app/views/layouts/base_mailer.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= message.subject %>
5 |
6 |
7 | <%= stylesheet_link_tag 'mailer', media: 'all' %>
8 |
9 |
10 |
11 |
12 |
13 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | <%= yield %>
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/app/dashboards/department_dashboard.rb:
--------------------------------------------------------------------------------
1 | require 'administrate/base_dashboard'
2 |
3 | class DepartmentDashboard < Administrate::BaseDashboard
4 | # ATTRIBUTE_TYPES
5 | # a hash that describes the type of each of the model's fields.
6 | #
7 | # Each different type represents an Administrate::Field object,
8 | # which determines how the attribute is displayed
9 | # on pages throughout the dashboard.
10 | ATTRIBUTE_TYPES = {
11 | opportunities: Field::HasMany,
12 | id: Field::Number,
13 | name: Field::String,
14 | created_at: Field::DateTime,
15 | updated_at: Field::DateTime
16 | }.freeze
17 |
18 | # COLLECTION_ATTRIBUTES
19 | # an array of attributes that will be displayed on the model's index page.
20 | #
21 | # By default, it's limited to four items to reduce clutter on index pages.
22 | # Feel free to add, remove, or rearrange items.
23 | COLLECTION_ATTRIBUTES = [
24 | :opportunities,
25 | :id,
26 | :name,
27 | :created_at
28 | ].freeze
29 |
30 | # SHOW_PAGE_ATTRIBUTES
31 | # an array of attributes that will be displayed on the model's show page.
32 | SHOW_PAGE_ATTRIBUTES = [
33 | :opportunities,
34 | :id,
35 | :name,
36 | :created_at,
37 | :updated_at
38 | ].freeze
39 |
40 | # FORM_ATTRIBUTES
41 | # an array of attributes that will be displayed
42 | # on the model's form (`new` and `edit`) pages.
43 | FORM_ATTRIBUTES = [
44 | :opportunities,
45 | :name
46 | ].freeze
47 |
48 | # Overwrite this method to customize how departments are displayed
49 | # across all pages of the admin dashboard.
50 | #
51 | def display_resource(department)
52 | "#{department.name} (##{department.id})"
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/app/views/opportunities/_admin_status.html.erb:
--------------------------------------------------------------------------------
1 | <% if @opportunity.approved? %>
2 |
3 |
4 | <%= t('opportunity_admin_status.waiting_for_publish_date', timestamp: long_timestamp(@opportunity.publish_at)).html_safe %>
5 |
6 | <% if policy(@opportunity).edit? %>
7 |
8 | <%= t('edit') %>
9 |
10 | <% end %>
11 |
12 | <% elsif policy(@opportunity).approve? %>
13 |
20 | <% elsif policy(@opportunity).edit? %>
21 |
22 |
23 | <% if @opportunity.submitted_for_approval? %>
24 | <%= t('opportunity_admin_status.submitted_for_approval', timestamp: long_timestamp(@opportunity.submitted_at)).html_safe %>
25 | <% else %>
26 | <%= t('opportunity_admin_status.not_approved') %>
27 | <% end %>
28 |
29 |
30 | <%= t('edit') %>
31 |
32 | <% if !@opportunity.submitted_for_approval? %>
33 | <%= t('request_approval') %>
34 | <% end %>
35 |
36 |
37 | <% end %>
38 |
--------------------------------------------------------------------------------
/app/uploaders/attachment_uploader.rb:
--------------------------------------------------------------------------------
1 | class AttachmentUploader < BaseUploader
2 | include CarrierWave::MiniMagick
3 |
4 | process :save_attributes_in_model
5 |
6 | version :thumb, if: :thumbable? do
7 | process :make_thumbnail
8 |
9 | # Ensure that thumbs are saved as pngs rather than the original format
10 | def full_filename(*args)
11 | filename = super(*args)
12 | File.basename(filename, File.extname(filename)) + '.png'
13 | end
14 | end
15 |
16 | protected
17 |
18 | def thumbable?(file)
19 | content_type = file.try(:content_type) || model.try(:content_type)
20 | content_type && content_type.match(/image|pdf/)
21 | end
22 |
23 | def make_thumbnail
24 | manipulate! do |image|
25 | image.format('png', 0) { |c| c.thumbnail('256x256') }
26 |
27 | # Remove transparency from PDFs
28 | if content_type['pdf']
29 | image.background 'white'
30 | image.alpha 'remove'
31 | else
32 | image
33 | end
34 | end
35 |
36 | model.has_thumbnail = true
37 |
38 | # See https://github.com/carrierwaveuploader/carrierwave/issues/617
39 | file.instance_variable_set(:@content_type, 'image/png')
40 |
41 | # Using exceptions for control flow is hacky, this could be refactored:
42 | # see https://github.com/dobtco/screendoor-v2/commit/8691546fea20fbae0929a0564784bef8c9180aad
43 | rescue CarrierWave::ProcessingError => e
44 | Rails.logger.warn(
45 | "[AttachmentUploader] Error generating thumbnail: #{e.message}"
46 | )
47 | end
48 |
49 | def save_attributes_in_model
50 | model.content_type = file.try(:content_type)
51 | model.file_size_bytes = file.try(:size)
52 | model.text_content = Yomu.read :text, file.read
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
We're sorry, but something went wrong.
62 |
63 |
If you are the application owner check the logs for more information.
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/spec/factories/users.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: users
4 | #
5 | # id :integer not null, primary key
6 | # name :string
7 | # email :string default(""), not null
8 | # encrypted_password :string default(""), not null
9 | # reset_password_token :string
10 | # reset_password_sent_at :datetime
11 | # remember_created_at :datetime
12 | # sign_in_count :integer default(0), not null
13 | # current_sign_in_at :datetime
14 | # last_sign_in_at :datetime
15 | # current_sign_in_ip :inet
16 | # last_sign_in_ip :inet
17 | # confirmation_token :string
18 | # confirmed_at :datetime
19 | # confirmation_sent_at :datetime
20 | # unconfirmed_email :string
21 | # permission_level :integer default(0), not null
22 | # business_name :string
23 | # business_data :string
24 | # created_at :datetime not null
25 | # updated_at :datetime not null
26 | #
27 | # Indexes
28 | #
29 | # index_users_on_confirmation_token (confirmation_token) UNIQUE
30 | # index_users_on_email (email) UNIQUE
31 | # index_users_on_reset_password_token (reset_password_token) UNIQUE
32 | #
33 |
34 | FactoryGirl.define do
35 | factory :user do
36 | name 'Joe Demo'
37 | sequence(:email) { |i| "user-#{i}@example.com" }
38 | password 'password'
39 | confirmed_at { Time.now }
40 |
41 | trait :staff do
42 | permission_level 'staff'
43 | end
44 |
45 | trait :approver do
46 | permission_level 'approver'
47 | end
48 |
49 | trait :admin do
50 | permission_level 'admin'
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The change you wanted was rejected.
62 |
Maybe you tried to change something you didn't have access to.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The page you were looking for doesn't exist.
62 |
You may have mistyped the address or the page may have moved.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/docs/deployment.md:
--------------------------------------------------------------------------------
1 | ## Deployment
2 |
3 | This guide assumes that you are deploying _your own fork_ of Dispatch, as per the [customization docs](customization.md).
4 |
5 | ### Heroku
6 |
7 | **1. Create a new application**
8 |
9 | `heroku create name_of_app`
10 |
11 | **2. Install add-ons:**
12 |
13 | - `heroku addons:create heroku-postgresql`
14 | - `heroku addons:create scheduler`
15 | - `heroku addons:create sendgrid`
16 | - `heroku addons:create newrelic` (optional)
17 |
18 |
19 | **3. Configure environment variables**
20 |
21 | Set the following variables by using `heroku config:set KEY=VAL`.
22 |
23 | ```
24 | # Configure a bucket on AWS for storing uploads:
25 | AWS_BUCKET: bucket-name
26 | AWS_REGION: us-west-2
27 | AWS_KEY: xxx
28 | AWS_SECRET: xxx
29 | UPLOAD_STORAGE: aws
30 |
31 | # The domain name of your app
32 | BASE_DOMAIN: dispatch-demo.herokuapp.com
33 | SSL: 1
34 |
35 | # Generate these values with `rake secret`
36 | SECRET_KEY_BASE: xxx
37 | SECRET_TOKEN: xxx
38 |
39 | SMTP_ADDRESS: smtp.sendgrid.net
40 | SMTP_AUTHENTICATION: plain
41 | SMTP_DOMAIN: heroku.com
42 |
43 | # Find this value with `heroku config:get SENDGRID_PASSWORD`
44 | SMTP_PASSWORD: xxx
45 |
46 | # Find this value with `heroku config:get SENDGRID_USERNAME`
47 | SMTP_USER: xxxxxxxxx@heroku.com
48 |
49 | SMTP_PORT: 587
50 | SMTP_STARTTLS_AUTO: 1
51 | ```
52 |
53 | **4. Configure your scheduled jobs:**
54 |
55 | Run `heroku addons:open scheduler` and configure your schedule to look like this: http://take.ms/938wy
56 |
57 | **5. Deploy it!**
58 |
59 | ```
60 | git push heroku master
61 | ```
62 |
63 | ### Other providers
64 |
65 | Deployment documentation for other providers may be added in the future.
66 |
--------------------------------------------------------------------------------
/app/views/opportunities/_search_results.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= sortable_table_header @opportunities, 'title', t('title') %>
5 | <%= sortable_table_header @opportunities, 'department', t('department') %>
6 | <%= sortable_table_header @opportunities, 'submissions_close_at', t('deadline') %>
7 |
8 |
9 | <% if @opportunities.present? %>
10 | <% @opportunities.each do |opportunity| %>
11 |
12 |
13 |
14 | <%= opportunity.title %>
15 |
16 |
17 | <% if opportunity.department %>
18 | <%= opportunity.department.name %>
19 | <% else %>
20 | <%= t('none') %>
21 | <% end %>
22 |
23 |
24 | <% if opportunity.submissions_close_at %>
25 | <%= long_timestamp(opportunity.submissions_close_at) %>
26 | <% else %>
27 | <%= t('none') %>
28 | <% end %>
29 |
30 |
31 |
32 |
33 | <% if opportunity.description.present? %>
34 | <%= truncate(strip_tags(opportunity.description), length: 250, escape: false) %>
35 | <% else %>
36 | <%= t('no_description') %>
37 | <% end %>
38 |
39 |
40 |
41 | <% end %>
42 | <% else %>
43 |
44 |
45 | <%= t('no_results') %>
46 |
47 |
48 | <% end %>
49 |
50 |
51 | <%= paginate @opportunities, pagination_class: 'pagination_centered' %>
52 |
--------------------------------------------------------------------------------
/app/views/opportunities/edit.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for(:page_key, 'opportunities-edit') %>
2 | <% content_for(:page_title, @opportunity.title) %>
3 | <% content_for(:main_container_class, 'container_small') %>
4 |
5 |
32 |
33 |
40 |
41 | <%= simple_form_for @opportunity, wrapper: :horizontal do |f| %>
42 |
43 |
44 | <%= render partial: "opportunities/edit/#{params[:step]}", locals: { f: f } %>
45 |
46 |
47 | <%= f.button :button, "#{t('save_and_continue')} →".html_safe, class: 'primary' %>
48 |
49 | <% end %>
50 |
--------------------------------------------------------------------------------
/app/dashboards/attachment_dashboard.rb:
--------------------------------------------------------------------------------
1 | require 'administrate/base_dashboard'
2 |
3 | class AttachmentDashboard < Administrate::BaseDashboard
4 | # ATTRIBUTE_TYPES
5 | # a hash that describes the type of each of the model's fields.
6 | #
7 | # Each different type represents an Administrate::Field object,
8 | # which determines how the attribute is displayed
9 | # on pages throughout the dashboard.
10 | ATTRIBUTE_TYPES = {
11 | opportunity: Field::BelongsTo,
12 | id: Field::Number,
13 | upload: Field::String,
14 | content_type: Field::String,
15 | file_size_bytes: Field::Number,
16 | has_thumbnail: Field::Boolean,
17 | deleted_at: Field::DateTime,
18 | created_at: Field::DateTime,
19 | updated_at: Field::DateTime
20 | }.freeze
21 |
22 | # COLLECTION_ATTRIBUTES
23 | # an array of attributes that will be displayed on the model's index page.
24 | #
25 | # By default, it's limited to four items to reduce clutter on index pages.
26 | # Feel free to add, remove, or rearrange items.
27 | COLLECTION_ATTRIBUTES = [
28 | :opportunity,
29 | :id,
30 | :upload,
31 | :content_type
32 | ].freeze
33 |
34 | # SHOW_PAGE_ATTRIBUTES
35 | # an array of attributes that will be displayed on the model's show page.
36 | SHOW_PAGE_ATTRIBUTES = [
37 | :opportunity,
38 | :id,
39 | :upload,
40 | :content_type,
41 | :file_size_bytes,
42 | :has_thumbnail,
43 | :deleted_at,
44 | :created_at,
45 | :updated_at
46 | ].freeze
47 |
48 | # FORM_ATTRIBUTES
49 | # an array of attributes that will be displayed
50 | # on the model's form (`new` and `edit`) pages.
51 | FORM_ATTRIBUTES = [
52 | :opportunity,
53 | :upload,
54 | :content_type,
55 | :file_size_bytes,
56 | :has_thumbnail,
57 | :deleted_at
58 | ].freeze
59 |
60 | # Overwrite this method to customize how attachments are displayed
61 | # across all pages of the admin dashboard.
62 | #
63 | # def display_resource(attachment)
64 | # "Attachment ##{attachment.id}"
65 | # end
66 | end
67 |
--------------------------------------------------------------------------------
/themes/dvl-core/assets/stylesheets/theme/overrides/rome.scss:
--------------------------------------------------------------------------------
1 | // Eventually, this should become part of dvl-core
2 |
3 | @import 'theme/includes';
4 |
5 | .rd-container {
6 | margin: $rhythm 0 ($rhythm * 2);
7 | border-color: $gray;
8 | border-radius: $radius;
9 | padding: 0 0 $rhythm * 1.5;
10 | font-size: $fontSmall;
11 | box-shadow: 0 1px 0 rgba(#000,0.06);
12 | td {
13 | padding: $rhythm / 2;
14 | }
15 | }
16 |
17 | .rd-month-label {
18 | background: $lightestGray;
19 | @include border_top_radius($radius - 1);
20 | padding: $rhythm ($rhythm * 2);
21 | }
22 |
23 | .rd-back,
24 | .rd-next {
25 | margin: $rhythm ($rhythm) 0;
26 | &:before {
27 | font-family: 'FontAwesome';
28 | font-size: 100%;
29 | color: $darkerGray;
30 | @include font_smoothing;
31 | }
32 | }
33 |
34 | .rd-days {
35 | padding: 0 ($rhythm * 2);
36 | margin-bottom: $rhythm;
37 | width: 100%;
38 | }
39 |
40 | .rd-days-head {
41 | th {
42 | padding: ($rhythm / 2) ($rhythm * 1.5);
43 | font-size: $fontSmallest;
44 | color: $darkerGray;
45 | @include font_smoothing;
46 | background: $lightestGray;
47 | border-bottom-color: $gray;
48 | }
49 | th:first-child {
50 | padding-left: $rhythm;
51 | }
52 | th:last-child {
53 | padding-right: $rhythm;
54 | }
55 | }
56 |
57 | .rd-days td {
58 | padding: $rhythm ($rhythm * 1.5);
59 | @include font_smoothing
60 | }
61 |
62 | .rd-back:before {
63 | content: '\f060';
64 | }
65 |
66 | .rd-next:before {
67 | content: '\f061';
68 | }
69 |
70 | .rd-day-selected,
71 | .rd-time-selected {
72 | background: $primaryColor;
73 | @include font_smoothing;
74 | }
75 |
76 | .rd-time-selected {
77 | border-radius: $radius;
78 | }
79 |
80 | .rd-time-list {
81 | color: $darkestGray;
82 | @include border-bottom_radius($radius);
83 | border: 1px solid $gray;
84 | border-top: 0;
85 | box-shadow: 0 1px 0 rgba(#000,0.06);
86 | margin-bottom: $rhythm * 2;
87 | }
88 |
89 | .rd-time-option:hover {
90 | background: rgba($primaryColor,0.2);
91 | color: $darkestGray;
92 | }
93 |
--------------------------------------------------------------------------------
/lib/tasks/auto_annotate_models.rake:
--------------------------------------------------------------------------------
1 | # NOTE: only doing this in development as some production environments (Heroku)
2 | # NOTE: are sensitive to local FS writes, and besides -- it's just not proper
3 | # NOTE: to have a dev-mode tool do its thing in production.
4 | if Rails.env.development?
5 | task :set_annotation_options do
6 | # You can override any of these by setting an environment variable of the
7 | # same name.
8 | Annotate.set_defaults(
9 | 'routes' => 'false',
10 | 'position_in_routes' => 'before',
11 | 'position_in_class' => 'before',
12 | 'position_in_test' => 'before',
13 | 'position_in_fixture' => 'before',
14 | 'position_in_factory' => 'before',
15 | 'position_in_serializer' => 'before',
16 | 'show_foreign_keys' => 'false',
17 | 'show_indexes' => 'true',
18 | 'simple_indexes' => 'false',
19 | 'model_dir' => 'app/models',
20 | 'root_dir' => '',
21 | 'include_version' => 'false',
22 | 'require' => '',
23 | 'exclude_tests' => 'false',
24 | 'exclude_fixtures' => 'false',
25 | 'exclude_factories' => 'false',
26 | 'exclude_serializers' => 'false',
27 | 'exclude_scaffolds' => 'false',
28 | 'exclude_controllers' => 'true',
29 | 'exclude_helpers' => 'true',
30 | 'ignore_model_sub_dir' => 'false',
31 | 'ignore_columns' => nil,
32 | 'ignore_unknown_models' => 'false',
33 | 'hide_limit_column_types' => 'integer,boolean',
34 | 'skip_on_db_migrate' => 'false',
35 | 'format_bare' => 'true',
36 | 'format_rdoc' => 'false',
37 | 'format_markdown' => 'false',
38 | 'sort' => 'false',
39 | 'force' => 'false',
40 | 'trace' => 'false',
41 | 'wrapper_open' => nil,
42 | 'wrapper_close' => nil
43 | )
44 | end
45 |
46 | Annotate.load_tasks
47 | end
48 |
--------------------------------------------------------------------------------
/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | include Pundit
3 |
4 | before_action :configure_permitted_parameters, if: :devise_controller?
5 | before_action :prepend_view_paths
6 | after_action :verify_authorized, unless: :devise_controller?
7 |
8 | # Prevent CSRF attacks by raising an exception.
9 | # For APIs, you may want to use :null_session instead.
10 | protect_from_forgery with: :exception
11 |
12 | rescue_from ActiveRecord::RecordNotFound,
13 | with: :not_found
14 |
15 | rescue_from Pundit::NotAuthorizedError,
16 | with: :deny_access
17 |
18 | def not_found(_e = nil)
19 | raise ActionController::RoutingError, 'Not Found'
20 | end
21 |
22 | def deny_access(*)
23 | respond_to do |format|
24 | format.any(:js, :json, :xml) { head :unauthorized }
25 | format.any do
26 | if signed_in?
27 | redirect_to root_path, error: t('access_denied')
28 | else
29 | redirect_to new_user_session_path
30 | end
31 | end
32 | end
33 | end
34 |
35 | def authorize_staff
36 | skip_authorization
37 | deny_access unless current_user.try(:permission_level_is_at_least?, 'staff')
38 | end
39 |
40 | private
41 |
42 | def configure_permitted_parameters
43 | devise_parameter_sanitizer.permit(:sign_up) do |u|
44 | u.permit(
45 | :email,
46 | :name,
47 | :password,
48 | :business_name,
49 | subscribe_to_category_ids: [],
50 | business_data: Array(u[:business_data].try(:keys))
51 | )
52 | end
53 |
54 | devise_parameter_sanitizer.permit(:account_update) do |u|
55 | u.permit(
56 | :email,
57 | :name,
58 | :password,
59 | :password_confirmation,
60 | :current_password,
61 | :business_name,
62 | business_data: Array(u[:business_data].try(:keys))
63 | )
64 | end
65 | end
66 |
67 | before_filter :prepend_view_paths
68 |
69 | def prepend_view_paths
70 | prepend_view_path DispatchConfiguration.theme_path.join('views')
71 | end
72 | end
73 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | AllCops:
2 | TargetRubyVersion: 2.3
3 | Exclude:
4 | - bin/**/*
5 | - db/**/*
6 | - config/initializers/**/*
7 | - vendor/**/*
8 | - tmp/**/*
9 |
10 | ## Setting some preferences...
11 |
12 | DotPosition:
13 | EnforcedStyle: trailing
14 |
15 | ## Disabling some cops:
16 |
17 | Documentation:
18 | Enabled: false
19 |
20 | IfUnlessModifier:
21 | Enabled: false
22 |
23 | # Disabling cops that came with an upgraded version of Rubocop -- feel free
24 | # to fix and re-enable these in the future, pending preference.
25 |
26 | Bundler/OrderedGems:
27 | Enabled: false
28 |
29 | Style/PercentLiteralDelimiters:
30 | Enabled: false
31 |
32 | Style/SafeNavigation:
33 | Enabled: false
34 |
35 | Style/SymbolArray:
36 | Enabled: false
37 |
38 | Style/RescueStandardError:
39 | Enabled: false
40 |
41 | Style/StringLiterals:
42 | Enabled: false
43 |
44 | Metrics/LineLength:
45 | Enabled: false
46 |
47 | Style/For:
48 | Enabled: false
49 |
50 | Metrics/BlockLength:
51 | Enabled: false
52 |
53 | Lint/AmbiguousBlockAssociation:
54 | Enabled: false
55 |
56 | Style/EmptyMethod:
57 | Enabled: false
58 |
59 | Style/MethodMissing:
60 | Enabled: false
61 |
62 | Layout/EmptyLineAfterMagicComment:
63 | Enabled: false
64 |
65 | # Using explicit #freeze seems to not get picked up by this cop?
66 | FrozenStringLiteralComment:
67 | Enabled: false
68 |
69 | # Causes issues with Rails' #update method
70 | RedundantMerge:
71 | Enabled: false
72 |
73 | StructInheritance:
74 | Enabled: false
75 |
76 | MultilineMethodCallIndentation:
77 | Enabled: false
78 |
79 | MultilineOperationIndentation:
80 | Enabled: false
81 |
82 | GuardClause:
83 | Enabled: false
84 |
85 | # https://github.com/bbatsov/ruby-style-guide/issues/270 kinda sums it up
86 | Lambda:
87 | Enabled: false
88 |
89 | ## We'll use CodeClimate or another tool to track the following:
90 |
91 | MethodLength:
92 | Enabled: false
93 |
94 | PerceivedComplexity:
95 | Enabled: false
96 |
97 | AbcSize:
98 | Enabled: false
99 |
100 | ClassLength:
101 | Enabled: false
102 |
--------------------------------------------------------------------------------
/spec/models/user_spec.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: users
4 | #
5 | # id :integer not null, primary key
6 | # name :string
7 | # email :string default(""), not null
8 | # encrypted_password :string default(""), not null
9 | # reset_password_token :string
10 | # reset_password_sent_at :datetime
11 | # remember_created_at :datetime
12 | # sign_in_count :integer default(0), not null
13 | # current_sign_in_at :datetime
14 | # last_sign_in_at :datetime
15 | # current_sign_in_ip :inet
16 | # last_sign_in_ip :inet
17 | # confirmation_token :string
18 | # confirmed_at :datetime
19 | # confirmation_sent_at :datetime
20 | # unconfirmed_email :string
21 | # permission_level :integer default(0), not null
22 | # business_name :string
23 | # business_data :string
24 | # created_at :datetime not null
25 | # updated_at :datetime not null
26 | #
27 | # Indexes
28 | #
29 | # index_users_on_confirmation_token (confirmation_token) UNIQUE
30 | # index_users_on_email (email) UNIQUE
31 | # index_users_on_reset_password_token (reset_password_token) UNIQUE
32 | #
33 |
34 | require 'spec_helper'
35 |
36 | describe User do
37 | subject { build(:user) }
38 | it { should be_valid }
39 |
40 | describe '#permission_level_is_at_least?' do
41 | it 'functions properly' do
42 | expect(subject.permission_level_is_at_least?('user')).to eq true
43 | expect(subject.permission_level_is_at_least?('approver')).to eq false
44 | end
45 | end
46 |
47 | describe 'automatic staff permissions' do
48 | context 'when user email matches a staff domain' do
49 | before do
50 | subject.email = 'foo@dispatch.gov'
51 | subject.save
52 | end
53 |
54 | it { should be_staff }
55 | end
56 |
57 | context 'when user email does not match a staff domain' do
58 | before do
59 | subject.email = 'foo@a-dispatch.gov'
60 | subject.save
61 | end
62 |
63 | it { should_not be_staff }
64 | end
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/app/controllers/questions_controller.rb:
--------------------------------------------------------------------------------
1 | class QuestionsController < ApplicationController
2 | include ActionView::RecordIdentifier
3 |
4 | before_action :set_opportunity
5 | before_action :set_question
6 |
7 | def create
8 | authorize @opportunity, :ask_question?
9 | @question = @opportunity.questions.build(ask_question_params)
10 |
11 | if @question.save && !asking_about_own_opportunity?
12 | Mailer.question_asked(
13 | @opportunity.created_by_user,
14 | @question
15 | ).deliver_later
16 | end
17 |
18 | redirect_to opportunity_path(@opportunity) + "##{dom_id(@question)}"
19 | end
20 |
21 | def update
22 | authorize @opportunity, :answer_questions?
23 | @question.update(answer_question_params)
24 |
25 | if @question.answered? && !answering_own_question?
26 | Mailer.question_answered(
27 | @question.asked_by_user,
28 | @question
29 | ).deliver_later
30 | end
31 |
32 | redirect_to opportunity_path(@opportunity) + "##{dom_id(@question)}"
33 | end
34 |
35 | def destroy
36 | authorize @opportunity, :answer_questions?
37 | @question.trash!
38 | redirect_to :back, info: t('question_destroyed')
39 | end
40 |
41 | private
42 |
43 | def set_opportunity
44 | @opportunity = Opportunity.find(params[:opportunity_id])
45 | end
46 |
47 | def set_question
48 | if params[:id]
49 | @question = @opportunity.questions.find(params[:id])
50 | end
51 | end
52 |
53 | def ask_question_params
54 | params.require(:question).permit(
55 | :question_text
56 | ).merge(
57 | asked_by_user: current_user
58 | )
59 | end
60 |
61 | def answer_question_params
62 | params.require(:question).permit(
63 | :answer_text
64 | ).merge(
65 | answered_by_user: current_user
66 | ).tap do |h|
67 | h[:answered_at] = if h[:answer_text].present?
68 | @question.answered_at || Time.now
69 | end
70 | end
71 | end
72 |
73 | def asking_about_own_opportunity?
74 | @question.asked_by_user == @opportunity.created_by_user
75 | end
76 |
77 | def answering_own_question?
78 | @question.asked_by_user == current_user
79 | end
80 | end
81 |
--------------------------------------------------------------------------------
/app/views/opportunities/_filter_form.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 | <%= simple_form_for :opportunity_filters, action: url_for, method: :get do |f| %>
10 |
11 |
12 |
13 | <%= f.input :text, required: false, input_html: { value: @opportunities.filterer.params[:text] } %>
14 | <%= f.input :status, required: false, as: :select, collection: [['Open for submissions', 'open'], ['Closed for submissions', 'closed'], ['Open or closed', 'all']], include_blank: false, selected: @opportunities.filterer.params[:status], input_html: { 'data-width' => 'full' } %>
15 | <%= f.input :category_ids, required: false, as: :select, collection: Category.all, selected: @opportunities.filterer.params[:category_ids], input_html: { multiple: true, 'data-no-styled-select' => true } %>
16 | <%= f.input :department_id, required: false, as: :select, collection: Department.all, selected: @opportunities.filterer.params[:department_id], include_blank: true %>
17 |
18 | <%= f.button :button, t('filter'), class: 'primary' %>
19 | <% end %>
20 |
21 |
22 |
23 | <% if filtered? %>
24 |
39 | <% end %>
40 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | ruby '2.3.1'
4 |
5 | gem 'rails', '4.2.7.1'
6 |
7 | gem 'administrate', '~> 0.2.2'
8 | gem 'autoprefixer-rails',
9 | git: 'https://github.com/ajb/autoprefixer-rails.git',
10 | branch: 'bundle-process'
11 | gem 'bourbon'
12 | gem 'carrierwave'
13 | gem 'carrierwave-aws'
14 | gem 'coffee-rails'
15 | gem 'delayed_job_active_record'
16 | gem 'devise'
17 | gem 'execjs'
18 | gem 'factory_girl_rails'
19 | gem 'filterer'
20 | gem 'font-awesome-rails'
21 | gem 'kaminari'
22 | gem 'mini_magick'
23 | gem 'newrelic_rpm'
24 | gem 'pg'
25 | gem 'pg_search'
26 | gem 'pundit'
27 | gem 'premailer-rails'
28 |
29 | # Locked: https://github.com/premailer/premailer/issues/315
30 | gem 'css_parser', '1.4.2'
31 |
32 | gem 'rails_oneline_logging'
33 | gem 'rinku'
34 | gem 'sassc-rails'
35 | gem 'simple_form'
36 | gem 'simple_form_legend'
37 | gem 'sprockets-rails'
38 | gem 'storage_unit'
39 | gem 'thin', require: false
40 | gem 'uglifier'
41 | gem 'utf8-cleaner'
42 | gem 'whenever', require: false
43 | gem 'yomu'
44 |
45 | gem 'dvl-core', git: 'https://github.com/dobtco/dvl-core.git'
46 | gem 'dvl-kaminari-views'
47 |
48 | source 'https://rails-assets.org' do
49 | gem 'rails-assets-inline_file_upload'
50 | gem 'rails-assets-jquery-form'
51 | gem 'rails-assets-jquery-timeago'
52 | gem 'rails-assets-jquery-ujs'
53 | gem 'rails-assets-jquery'
54 | gem 'rails-assets-rome'
55 | gem 'rails-assets-selectize'
56 | gem 'rails-assets-underscore'
57 | end
58 |
59 | group :development do
60 | gem 'annotate'
61 | gem 'better_errors'
62 | gem 'binding_of_caller'
63 | gem 'guard-livereload', require: false
64 | gem 'guard-rspec'
65 | gem 'guard-rubocop'
66 | gem 'launchy'
67 | gem 'letter_opener'
68 | gem 'spring-commands-rspec'
69 | gem 'spring'
70 | end
71 |
72 | group :test do
73 | gem 'brakeman', require: false
74 | gem 'capybara'
75 | gem 'capybara-webkit'
76 | gem 'codeclimate-test-reporter', '~> 0.6.0', require: false
77 | gem 'database_cleaner'
78 | gem 'rspec-its'
79 | gem 'webmock'
80 | end
81 |
82 | group :development, :test do
83 | gem 'i18n-tasks'
84 | gem 'pry'
85 | gem 'rspec-rails'
86 | gem 'rubocop', '~> 0.49', require: false
87 | end
88 |
89 | group :production do
90 | gem 'rails_12factor'
91 | end
92 |
--------------------------------------------------------------------------------
/config.yml.example:
--------------------------------------------------------------------------------
1 | # Any key that takes a string as its value can be overridden by
2 | # setting an environment variable of the same name.
3 |
4 | # What is the site called? What government entity is it being used by?
5 | # These keys will be used in views and outgoing emails.
6 | SITE_TITLE: Dispatch
7 | AGENCY_NAME: The City of Philadelphia
8 |
9 | # This name must correspond to a subdirectory in the themes/ directory
10 | THEME: dvl-core
11 |
12 | FOOTER_LINKS:
13 | - text: Contact us
14 | href: 'mailto:support@dobt.co'
15 |
16 | # Each entry must correspond to a template in views/static/#{path}.html.erb
17 | STATIC_PAGES:
18 | - name: About
19 | path: about
20 |
21 | # Valid options: file, aws
22 | UPLOAD_STORAGE: file
23 |
24 | # AWS requires these settings:
25 | # AWS_KEY:
26 | # AWS_SECRET:
27 | # AWS_REGION:
28 | # AWS_BUCKET:
29 |
30 | # Where do notifications get sent from?
31 | EMAIL_NOTIFICATION_FROM_ADDRESS: noreply@dobt.co
32 |
33 | # How long before the question deadline should we remind subscribers?
34 | QUESTION_DEADLINE_REMINDER_HOURS: 48
35 |
36 | # How long before the submission deadline should we remind subscribers?
37 | SUBMISSION_DEADLINE_REMINDER_HOURS: 72
38 |
39 | # Users with an email address that ends in one of these domains will
40 | # automatically be granted `staff` permission
41 | STAFF_DOMAINS:
42 | - dispatch.gov
43 |
44 | # *Highly* recommended in production
45 | SSL: false
46 |
47 | # The domain of your site, e.g. "www.dispatch.gov". Don't include the `https`
48 | BASE_DOMAIN: localhost:3000
49 |
50 | # Rails cookie-signing secrets. You cangGenerate values with `rake secret`
51 | # for deployed environments.
52 | SECRET_TOKEN: this is an insecure value that must...
53 | SECRET_KEY_BASE: ...be changed in production!
54 |
55 | # Use a CDN
56 | # ASSET_HOST:
57 |
58 | # In staging, redirect all email that does not match the address whitelist
59 | # to this user. Used for preventing users from receiving emails sent
60 | # by a staging server.
61 | #
62 | # REDIRECT_EMAIL_TO: noreply@dobt.co
63 |
64 | # SMTP configuration for sending email in deployed environments. This must be
65 | # configured in staging and production!
66 | #
67 | # SMTP_ADDRESS:
68 | # SMTP_PORT:
69 | # SMTP_STARTTLS_AUTO:
70 | # SMTP_USER:
71 | # SMTP_PASSWORD:
72 | # SMTP_AUTHENTICATION:
73 | # SMTP_DOMAIN:
74 | # SMTP_SSL:
75 |
--------------------------------------------------------------------------------
/app/mailers/mailer.rb:
--------------------------------------------------------------------------------
1 | class Mailer < ActionMailer::Base
2 | include BuildEmailAddressHelper
3 | helper FormattingHelper
4 |
5 | before_action { @include_subscription_preferences_link = true }
6 |
7 | layout 'base_mailer'
8 |
9 | def search_results(user, opportunity_ids)
10 | @opportunities = Opportunity.
11 | where(id: opportunity_ids).
12 | order_by_recently_posted
13 |
14 | build_email(user)
15 | end
16 |
17 | def question_asked(user, question)
18 | @question = question
19 | build_email(user)
20 | end
21 |
22 | def question_answered(user, question)
23 | @question = question
24 | build_email(user)
25 | end
26 |
27 | def approval_request(user, opportunity)
28 | @opportunity = opportunity
29 | @creator = opportunity.created_by_user
30 |
31 | build_email(
32 | user,
33 | subject: t(
34 | 'mailer.approval_request.subject',
35 | creator: @creator.name,
36 | site_title: DispatchConfiguration.site_title
37 | ),
38 | reply_to: opportunity.created_by_user.email
39 | )
40 | end
41 |
42 | def question_deadline(user, opportunity)
43 | @opportunity = opportunity
44 |
45 | build_email(
46 | user,
47 | subject: t(
48 | 'mailer.question_deadline.subject',
49 | opportunity: @opportunity.title
50 | )
51 | )
52 | end
53 |
54 | def submission_deadline(user, opportunity)
55 | @opportunity = opportunity
56 |
57 | build_email(
58 | user,
59 | subject: t(
60 | 'mailer.submission_deadline.subject',
61 | opportunity: @opportunity.title
62 | )
63 | )
64 | end
65 |
66 | private
67 |
68 | def build_email(user, mail_params = {})
69 | @user = user
70 | mail_params[:from] ||= default_from_address
71 | mail_params[:to] ||= build_email_address(user.email, user.name)
72 | mail_params[:subject] ||= calculate_subject
73 | mail(mail_params)
74 | end
75 |
76 | def calculate_subject
77 | t(
78 | "mailer.#{action_name}.subject",
79 | site_title: DispatchConfiguration.site_title
80 | )
81 | end
82 |
83 | def default_from_address
84 | build_email_address(
85 | DispatchConfiguration.email_notification_from_address,
86 | DispatchConfiguration.site_title
87 | )
88 | end
89 | end
90 |
--------------------------------------------------------------------------------
/app/dashboards/question_dashboard.rb:
--------------------------------------------------------------------------------
1 | require 'administrate/base_dashboard'
2 |
3 | class QuestionDashboard < Administrate::BaseDashboard
4 | # ATTRIBUTE_TYPES
5 | # a hash that describes the type of each of the model's fields.
6 | #
7 | # Each different type represents an Administrate::Field object,
8 | # which determines how the attribute is displayed
9 | # on pages throughout the dashboard.
10 | ATTRIBUTE_TYPES = {
11 | opportunity: Field::BelongsTo,
12 | asked_by_user: Field::BelongsTo.with_options(class_name: 'User'),
13 | answered_by_user: Field::BelongsTo.with_options(class_name: 'User'),
14 | id: Field::Number,
15 | asked_by_user_id: Field::Number,
16 | answered_by_user_id: Field::Number,
17 | question_text: Field::Text,
18 | answer_text: Field::Text,
19 | answered_at: Field::DateTime,
20 | deleted_at: Field::DateTime,
21 | created_at: Field::DateTime,
22 | updated_at: Field::DateTime
23 | }.freeze
24 |
25 | # COLLECTION_ATTRIBUTES
26 | # an array of attributes that will be displayed on the model's index page.
27 | #
28 | # By default, it's limited to four items to reduce clutter on index pages.
29 | # Feel free to add, remove, or rearrange items.
30 | COLLECTION_ATTRIBUTES = [
31 | :opportunity,
32 | :asked_by_user,
33 | :answered_by_user,
34 | :id
35 | ].freeze
36 |
37 | # SHOW_PAGE_ATTRIBUTES
38 | # an array of attributes that will be displayed on the model's show page.
39 | SHOW_PAGE_ATTRIBUTES = [
40 | :opportunity,
41 | :asked_by_user,
42 | :answered_by_user,
43 | :id,
44 | :asked_by_user_id,
45 | :answered_by_user_id,
46 | :question_text,
47 | :answer_text,
48 | :answered_at,
49 | :deleted_at,
50 | :created_at,
51 | :updated_at
52 | ].freeze
53 |
54 | # FORM_ATTRIBUTES
55 | # an array of attributes that will be displayed
56 | # on the model's form (`new` and `edit`) pages.
57 | FORM_ATTRIBUTES = [
58 | :opportunity,
59 | :asked_by_user,
60 | :answered_by_user,
61 | :asked_by_user_id,
62 | :answered_by_user_id,
63 | :question_text,
64 | :answer_text,
65 | :answered_at,
66 | :deleted_at
67 | ].freeze
68 |
69 | # Overwrite this method to customize how questions are displayed
70 | # across all pages of the admin dashboard.
71 | #
72 | # def display_resource(question)
73 | # "Question ##{question.id}"
74 | # end
75 | end
76 |
--------------------------------------------------------------------------------
/app/views/questions/_question.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= t('asked_by_user_at_time', user: question.asked_by_user.try(:name) || 'Unknown', timestamp: long_timestamp(question.created_at)).html_safe %>
5 |
6 |
<%= format_textarea_input(question.question_text) %>
7 |
8 |
9 |
10 | <% if policy(@opportunity).answer_questions? %>
11 |
12 | <%= simple_form_for [@opportunity, question], html: { style: 'display:none', 'data-toggle' => "edit-answer-#{question.id}" } do |f| %>
13 | <%= f.input_field :answer_text, 'aria-label' => t('answer_text'), placeholder: t('write_your_answer_here') %>
14 | <% if question.answered? %>
15 | <%= f.button :button, t('save_changes'), class: 'primary small' %>
16 | <% else %>
17 | <%= f.button :button, t('publish'), class: 'primary small' %>
18 | <% end %>
19 | <% end %>
20 |
21 | <% end %>
22 |
23 | <% if question.answered? %>
24 |
25 |
26 | <%= t('answered_by_user_at_time', user: question.answered_by_user.try(:name) || 'Unknown', timestamp: long_timestamp(question.answered_at)).html_safe %>
27 |
28 |
<%= format_textarea_input(question.answer_text) %>
29 |
30 | <% end %>
31 |
32 | <% if policy(@opportunity).answer_questions? %>
33 |
50 | <% end %>
51 |
52 |
53 |
--------------------------------------------------------------------------------