├── log └── .keep ├── app ├── mailers │ ├── .keep │ └── thing_mailer.rb ├── models │ ├── .keep │ ├── concerns │ │ └── .keep │ ├── address.rb │ ├── reminder.rb │ ├── user.rb │ └── thing.rb ├── assets │ ├── images │ │ ├── .keep │ │ ├── lock.png │ │ ├── logos │ │ │ ├── cfa.png │ │ │ ├── boston.png │ │ │ ├── bostonbuilt.png │ │ │ ├── adopt-a-hydrant.png │ │ │ └── adopt-a-hydrant_large.png │ │ └── markers │ │ │ ├── red.png │ │ │ ├── green.png │ │ │ ├── marker.psd │ │ │ └── shadow.png │ ├── javascripts │ │ ├── application.js │ │ └── main.js.erb │ └── stylesheets │ │ ├── application.css │ │ └── screen.css ├── controllers │ ├── concerns │ │ └── .keep │ ├── main_controller.rb │ ├── sidebar_controller.rb │ ├── sitemaps_controller.rb │ ├── addresses_controller.rb │ ├── info_window_controller.rb │ ├── reminders_controller.rb │ ├── things_controller.rb │ ├── application_controller.rb │ ├── sessions_controller.rb │ ├── passwords_controller.rb │ └── users_controller.rb ├── helpers │ ├── users_helper.rb │ ├── welcome_helper.rb │ ├── reminders_helper.rb │ ├── sessions_helper.rb │ └── application_helper.rb └── views │ ├── sidebar │ ├── search.html.haml │ ├── combo_form.html.haml │ ├── _search.html.haml │ ├── _combo_form.html.haml │ ├── edit_profile.html.haml │ └── _tos.html.haml │ ├── users │ ├── sign_in.html.haml │ ├── thank_you.html.haml │ ├── _reminder.html.haml │ └── profile.html.haml │ ├── layouts │ ├── sidebar.html.haml │ ├── info_window.html.haml │ ├── _flash.html.haml │ └── application.html.haml │ ├── sitemaps │ └── index.xml.haml │ ├── thing_mailer │ └── reminder.text.erb │ ├── things │ ├── _abandon.html.haml │ └── adopt.html.haml │ ├── passwords │ └── edit.html.haml │ └── main │ ├── unauthenticated.html.haml │ └── index.html.haml ├── lib ├── assets │ └── .keep └── tasks │ └── .keep ├── test ├── helpers │ └── .keep ├── mailers │ └── .keep ├── models │ └── .keep ├── controllers │ ├── .keep │ ├── sitemaps_controller_test.rb │ ├── things_controller_test.rb │ ├── sessions_controller_test.rb │ ├── reminders_controller_test.rb │ ├── addresses_controller_test.rb │ ├── main_controller_test.rb │ ├── users_controller_test.rb │ ├── passwords_controller_test.rb │ └── info_window_controller_test.rb ├── fixtures │ ├── .keep │ ├── unknown_address.json │ ├── reminders.yml │ ├── users.yml │ ├── things.yml │ └── city_hall.json ├── integration │ └── .keep ├── functional │ └── .gitkeep └── test_helper.rb ├── .ruby-version ├── vendor └── assets │ ├── javascripts │ └── .keep │ └── stylesheets │ └── .keep ├── Procfile ├── screenshot.png ├── public ├── favicon.ico ├── robots.txt ├── 500.html ├── 422.html └── 404.html ├── bin ├── bundle ├── rake ├── rails ├── spring └── setup ├── config ├── boot.rb ├── initializers │ ├── cookies_serializer.rb │ ├── session_store.rb │ ├── mime_types.rb │ ├── filter_parameter_logging.rb │ ├── rails_admin.rb │ ├── backtrace_silencers.rb │ ├── assets.rb │ ├── wrap_parameters.rb │ ├── inflections.rb │ ├── secret_token.rb │ └── devise.rb ├── environment.rb ├── unicorn.rb ├── database.yml ├── routes.rb ├── secrets.yml ├── application.rb ├── environments │ ├── development.rb │ ├── test.rb │ └── production.rb └── locales │ ├── en.yml │ ├── pt.yml │ ├── it.yml │ ├── es.yml │ ├── fr.yml │ ├── de.yml │ └── devise.en.yml ├── foo ├── config.ru ├── doc └── README_FOR_APP ├── Rakefile ├── script └── rails ├── db ├── migrate │ ├── 00000000000003_create_things.rb │ ├── 00000000000004_create_reminders.rb │ ├── 00000000000005_create_rails_admin_histories_table.rb │ ├── 00000000000001_create_users.rb │ └── 00000000000002_add_devise_to_users.rb └── schema.rb ├── .travis.yml ├── .gitignore ├── Gemfile ├── .rubocop.yml ├── LICENSE.md ├── README.md └── Gemfile.lock /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.3.1 2 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/controllers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/functional/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec puma -p $PORT 2 | -------------------------------------------------------------------------------- /app/helpers/users_helper.rb: -------------------------------------------------------------------------------- 1 | module UsersHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/welcome_helper.rb: -------------------------------------------------------------------------------- 1 | module WelcomeHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/reminders_helper.rb: -------------------------------------------------------------------------------- 1 | module RemindersHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/sessions_helper.rb: -------------------------------------------------------------------------------- 1 | module SessionsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/views/sidebar/search.html.haml: -------------------------------------------------------------------------------- 1 | = render :partial => "search" 2 | -------------------------------------------------------------------------------- /app/views/sidebar/combo_form.html.haml: -------------------------------------------------------------------------------- 1 | = render :partial => "combo_form" 2 | -------------------------------------------------------------------------------- /app/controllers/main_controller.rb: -------------------------------------------------------------------------------- 1 | class MainController < ApplicationController 2 | end 3 | -------------------------------------------------------------------------------- /app/controllers/sidebar_controller.rb: -------------------------------------------------------------------------------- 1 | class SidebarController < ApplicationController 2 | end 3 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/adopt-a-hydrant/HEAD/screenshot.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/adopt-a-hydrant/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /test/fixtures/unknown_address.json: -------------------------------------------------------------------------------- 1 | { 2 | "results" : [], 3 | "status" : "ZERO_RESULTS" 4 | } 5 | -------------------------------------------------------------------------------- /app/views/users/sign_in.html.haml: -------------------------------------------------------------------------------- 1 | %h2 2 | = t("titles.sign_in", :thing => t("defaults.thing").titleize) 3 | -------------------------------------------------------------------------------- /test/fixtures/reminders.yml: -------------------------------------------------------------------------------- 1 | --- 2 | reminder_1: 3 | from_user: erik 4 | to_user: dan 5 | thing: thing_1 6 | -------------------------------------------------------------------------------- /app/assets/images/lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/adopt-a-hydrant/HEAD/app/assets/images/lock.png -------------------------------------------------------------------------------- /app/views/layouts/sidebar.html.haml: -------------------------------------------------------------------------------- 1 | = render :partial => "layouts/flash", :locals => {:flash => flash} 2 | = yield 3 | -------------------------------------------------------------------------------- /app/controllers/sitemaps_controller.rb: -------------------------------------------------------------------------------- 1 | class SitemapsController < ApplicationController 2 | respond_to :xml 3 | end 4 | -------------------------------------------------------------------------------- /app/assets/images/logos/cfa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/adopt-a-hydrant/HEAD/app/assets/images/logos/cfa.png -------------------------------------------------------------------------------- /app/assets/images/markers/red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/adopt-a-hydrant/HEAD/app/assets/images/markers/red.png -------------------------------------------------------------------------------- /app/assets/images/logos/boston.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/adopt-a-hydrant/HEAD/app/assets/images/logos/boston.png -------------------------------------------------------------------------------- /app/assets/images/markers/green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/adopt-a-hydrant/HEAD/app/assets/images/markers/green.png -------------------------------------------------------------------------------- /app/assets/images/markers/marker.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/adopt-a-hydrant/HEAD/app/assets/images/markers/marker.psd -------------------------------------------------------------------------------- /app/assets/images/markers/shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/adopt-a-hydrant/HEAD/app/assets/images/markers/shadow.png -------------------------------------------------------------------------------- /app/views/layouts/info_window.html.haml: -------------------------------------------------------------------------------- 1 | #info_window 2 | = render :partial => "layouts/flash", :locals => {:flash => flash} 3 | = yield 4 | -------------------------------------------------------------------------------- /app/assets/images/logos/bostonbuilt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/adopt-a-hydrant/HEAD/app/assets/images/logos/bostonbuilt.png -------------------------------------------------------------------------------- /app/views/users/thank_you.html.haml: -------------------------------------------------------------------------------- 1 | %h2 2 | = t("titles.thank_you", :thing => t("defaults.thing")) 3 | = render :partial => 'things/abandon' 4 | -------------------------------------------------------------------------------- /app/assets/images/logos/adopt-a-hydrant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/adopt-a-hydrant/HEAD/app/assets/images/logos/adopt-a-hydrant.png -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /app/assets/images/logos/adopt-a-hydrant_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/adopt-a-hydrant/HEAD/app/assets/images/logos/adopt-a-hydrant_large.png -------------------------------------------------------------------------------- /foo: -------------------------------------------------------------------------------- 1 | VMW 2 | FB 3 | TWTR 4 | SQ 5 | DIS 6 | TWX 7 | WFM 8 | 9 | FRO 10 | NYMT 11 | PSEC 12 | STWD 13 | 14 | BA 15 | LMT 16 | RTN 17 | NOC 18 | GD 19 | LLL 20 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.action_dispatch.cookies_serializer = :json 4 | -------------------------------------------------------------------------------- /app/views/sitemaps/index.xml.haml: -------------------------------------------------------------------------------- 1 | !!! XML 2 | %urlset{:xmlns => "http://www.sitemaps.org/schemas/sitemap/0.9"} 3 | %url 4 | %loc 5 | = "http://#{request.env["HTTP_HOST"]}" 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_adopt-a-thing_session' 4 | -------------------------------------------------------------------------------- /app/models/address.rb: -------------------------------------------------------------------------------- 1 | class Address 2 | include Geokit::Geocoders 3 | 4 | def self.geocode(address) 5 | MultiGeocoder.geocode(address).ll.split(',').collect(&:to_f) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path("../spring", __FILE__) 4 | rescue LoadError 5 | end 6 | require_relative '../config/boot' 7 | require 'rake' 8 | Rake.application.run 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /doc/README_FOR_APP: -------------------------------------------------------------------------------- 1 | Use this README file to introduce your application and point to useful places in the API for learning more. 2 | Run "rake doc:app" to generate API documentation for your models, controllers, helpers, and libraries. 3 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path("../spring", __FILE__) 4 | rescue LoadError 5 | end 6 | APP_PATH = File.expand_path('../../config/application', __FILE__) 7 | require_relative '../config/boot' 8 | require 'rails/commands' 9 | -------------------------------------------------------------------------------- /test/controllers/sitemaps_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class SitemapsControllerTest < ActionController::TestCase 4 | test 'should return an XML sitemap' do 5 | get :index, format: 'xml' 6 | assert_response :success 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /config/initializers/rails_admin.rb: -------------------------------------------------------------------------------- 1 | RailsAdmin.config do |config| 2 | config.authenticate_with do 3 | redirect_to(main_app.root_path, flash: {warning: 'You must be signed-in as an administrator to access that page'}) unless signed_in? && current_user.admin? 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/mailers/thing_mailer.rb: -------------------------------------------------------------------------------- 1 | class ThingMailer < ActionMailer::Base 2 | default from: 'adoptahydrant@cityofboston.gov' 3 | 4 | def reminder(thing) 5 | @thing = thing 6 | @user = thing.user 7 | mail(to: thing.user.email, subject: ['Remember to shovel', thing.name].compact.join(' ')) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 3 | 4 | APP_PATH = File.expand_path('../../config/application', __FILE__) 5 | require File.expand_path('../../config/boot', __FILE__) 6 | require 'rails/commands' 7 | -------------------------------------------------------------------------------- /app/views/thing_mailer/reminder.text.erb: -------------------------------------------------------------------------------- 1 | Hello <%= @user.name.split.first %>, 2 | 3 | Did you remember to shovel out the fire hydrant you adopted at <%= @thing.street_address %>? If not, please shovel your hydrant as soon as possible! 4 | 5 | If you've already shoveled, thank you for serving your city! 6 | 7 | The Adopt-a-Hydrant Team 8 | -------------------------------------------------------------------------------- /app/models/reminder.rb: -------------------------------------------------------------------------------- 1 | class Reminder < ActiveRecord::Base 2 | include ActiveModel::ForbiddenAttributesProtection 3 | belongs_to :from_user, class_name: 'User' 4 | belongs_to :thing 5 | belongs_to :to_user, class_name: 'User' 6 | validates :from_user, presence: true 7 | validates :thing, presence: true 8 | validates :to_user, presence: true 9 | end 10 | -------------------------------------------------------------------------------- /app/views/things/_abandon.html.haml: -------------------------------------------------------------------------------- 1 | = form_for :thing, :url => things_path, :method => :put, :html => {:id => "abandon_form"} do |f| 2 | = f.hidden_field "id" 3 | = f.hidden_field "user_id", :value => "" 4 | = f.hidden_field "name", :value => "" 5 | %fieldset.control-group 6 | = f.submit t("buttons.abandon", :thing => t("defaults.thing")), :class => "btn danger" 7 | -------------------------------------------------------------------------------- /config/unicorn.rb: -------------------------------------------------------------------------------- 1 | worker_processes 4 2 | timeout 30 3 | preload_app true 4 | 5 | before_fork do |_server, _worker| 6 | Signal.trap 'TERM' do 7 | Process.kill 'QUIT', Process.pid 8 | end 9 | ActiveRecord::Base.connection.disconnect! 10 | end 11 | 12 | after_fork do |_server, _worker| 13 | Signal.trap 'TERM' do 14 | end 15 | ActiveRecord::Base.establish_connection 16 | end 17 | -------------------------------------------------------------------------------- /app/views/users/_reminder.html.haml: -------------------------------------------------------------------------------- 1 | = form_for :reminder, :url => reminders_path, :html => {:id => "reminder_form", :method => "post"} do |f| 2 | = f.hidden_field "from_user_id", :value => current_user.id 3 | = f.hidden_field "to_user_id", :value => @thing.user.id 4 | = f.hidden_field "thing_id", :value => @thing.id 5 | %fieldset.control-group 6 | = f.submit t("buttons.send_reminder"), :class => "btn" 7 | -------------------------------------------------------------------------------- /app/views/users/profile.html.haml: -------------------------------------------------------------------------------- 1 | %h2 2 | = t("titles.adopted", :thing_name => @thing.name ? @thing.name.titleize : t("defaults.this_thing", :thing => t("defaults.thing"))) 3 | %br 4 | = t("titles.byline", :name => @thing.user.name) 5 | %br 6 | = t("titles.ofline", :organization => @thing.user.organization) unless @thing.user.organization.blank? 7 | - if user_signed_in? 8 | = render :partial => 'users/reminder' 9 | -------------------------------------------------------------------------------- /app/controllers/addresses_controller.rb: -------------------------------------------------------------------------------- 1 | class AddressesController < ApplicationController 2 | respond_to :json 3 | 4 | def show 5 | @address = Address.geocode("#{params[:address]}, #{params[:city_state]}") 6 | if @address.blank? 7 | render(json: {errors: {address: [t('errors.not_found', thing: t('defaults.address'))]}}, status: 404) 8 | else 9 | respond_with @address 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/controllers/info_window_controller.rb: -------------------------------------------------------------------------------- 1 | class InfoWindowController < ApplicationController 2 | def index 3 | @thing = Thing.find_by_id(params[:thing_id]) 4 | view = begin 5 | if @thing.adopted? 6 | user_signed_in? && current_user == @thing.user ? 'users/thank_you' : 'users/profile' 7 | else 8 | user_signed_in? ? 'things/adopt' : 'users/sign_in' 9 | end 10 | end 11 | render view 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | development: &DEVELOPMENT 2 | adapter: postgresql 3 | database: adopt_a_thing_development 4 | host: localhost 5 | 6 | # Warning: The database defined as "test" will be erased and 7 | # re-generated from your development database when you run "rake". 8 | # Do not set this db to the same as development or production. 9 | test: 10 | <<: *DEVELOPMENT 11 | database: adopt_a_thing_test 12 | 13 | production: 14 | <<: *DEVELOPMENT 15 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /db/migrate/00000000000003_create_things.rb: -------------------------------------------------------------------------------- 1 | class CreateThings < ActiveRecord::Migration 2 | def change 3 | create_table :things do |t| 4 | t.timestamps 5 | t.string :name 6 | t.decimal :lat, null: false, precision: 16, scale: 14 7 | t.decimal :lng, null: false, precision: 17, scale: 14 8 | t.integer :city_id 9 | t.integer :user_id 10 | end 11 | 12 | add_index :things, :city_id, unique: true 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/views/passwords/edit.html.haml: -------------------------------------------------------------------------------- 1 | %h2 2 | = t("titles.edit_profile") 3 | = form_for resource, :as => resource_name, :url => password_path(resource_name), :html => {:id => "edit_form", :method => :put} do |f| 4 | = f.hidden_field "reset_password_token" 5 | %fieldset.control-group 6 | = f.label "password", t("labels.password_new"), :id => "user_password_label" 7 | = f.password_field "password" 8 | %fieldset.form-actions 9 | = f.submit t("buttons.change_password"), :class => "btn" 10 | -------------------------------------------------------------------------------- /test/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | --- 2 | erik: 3 | name: Erik Michaels-Ober 4 | organization: Code for America 5 | email: erik@example.com 6 | voice_number: 1234567890 7 | sms_number: 1234567890 8 | encrypted_password: "$2a$10$KF/JMZ494ZLhWLgHZeBTf.cSL4l0Wjij4gIZP7BzkueAC1p4nW0ma" 9 | 10 | dan: 11 | name: Dan Melton 12 | organization: Code for America 13 | email: dan@example.com 14 | voice_number: 1234567890 15 | sms_number: 1234567890 16 | encrypted_password: "$2a$10$KF/JMZ494ZLhWLgHZeBTf.cSL4l0Wjij4gIZP7BzkueAC1p4nW0ma" 17 | 18 | -------------------------------------------------------------------------------- /app/views/things/adopt.html.haml: -------------------------------------------------------------------------------- 1 | %h2 2 | = t("titles.adopt", :thing => t("defaults.thing").titleize) 3 | = form_for :thing, :url => things_path, :method => :put, :html => {:id => "adoption_form"} do |f| 4 | = f.hidden_field "id" 5 | = f.hidden_field "user_id", :value => current_user.id 6 | %fieldset.control-group 7 | = f.label "name", t("labels.name_thing", :thing => t("defaults.thing")), :id => "thing_name_label" 8 | = f.text_field "name" 9 | %fieldset.control-group 10 | = f.submit t("buttons.adopt"), :class => "btn primary" 11 | -------------------------------------------------------------------------------- /db/migrate/00000000000004_create_reminders.rb: -------------------------------------------------------------------------------- 1 | class CreateReminders < ActiveRecord::Migration 2 | def change 3 | create_table :reminders do |t| 4 | t.timestamps 5 | t.integer :from_user_id, null: false 6 | t.integer :to_user_id, null: false 7 | t.integer :thing_id, null: false 8 | t.boolean :sent, default: false 9 | end 10 | 11 | add_index :reminders, :from_user_id 12 | add_index :reminders, :to_user_id 13 | add_index :reminders, :thing_id 14 | add_index :reminders, :sent 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 11 | # Rails.application.config.assets.precompile += %w( search.js ) 12 | -------------------------------------------------------------------------------- /db/migrate/00000000000005_create_rails_admin_histories_table.rb: -------------------------------------------------------------------------------- 1 | class CreateRailsAdminHistoriesTable < ActiveRecord::Migration 2 | def change 3 | create_table :rails_admin_histories do |t| 4 | t.string :message # title, name, or object_id 5 | t.string :username 6 | t.integer :item 7 | t.string :table 8 | t.integer :month, limit: 2 9 | t.integer :year, limit: 5 10 | t.timestamps 11 | end 12 | 13 | add_index(:rails_admin_histories, [:item, :table, :month, :year], name: 'index_rails_admin_histories') 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /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 | ENV["GEM_PATH"] = ([Bundler.bundle_path.to_s] + Gem.path).join(File::PATH_SEPARATOR) 12 | ENV["GEM_HOME"] = "" 13 | Gem.paths = ENV 14 | 15 | gem "spring", match[1] 16 | require "spring/binstub" 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /db/migrate/00000000000001_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration 2 | def change 3 | create_table :users do |t| 4 | t.timestamps 5 | t.string :name, null: false 6 | t.string :organization 7 | t.string :email, null: false 8 | t.string :voice_number 9 | t.string :sms_number 10 | t.string :address_1 11 | t.string :address_2 12 | t.string :city 13 | t.string :state 14 | t.string :zip 15 | t.boolean :admin, default: false 16 | end 17 | 18 | add_index :users, :email, unique: true 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | before_script: bundle exec rake db:create db:schema:load 2 | bundler_args: "--without assets:development:production" 3 | cache: bundler 4 | language: ruby 5 | rvm: 6 | - 2.3.1 7 | script: 8 | - bundle exec rake 9 | - bundle exec rubocop 10 | sudo: false 11 | deploy: 12 | api_key: 13 | secure: Ck6BzHMtPmYsBY/PbVnfIE6wnSe9s6fYDkvzYZtT/2qud4j4ElhV9el0ZbDhiTmix0PMwcCnN/Tpw5GbLVaHncDaJkvb6ucEBNyC7xECjNAFsxE6lu3yKATsY2hta7OQ8NwLlvAZpzCLMZQf9lzoSNbh2h/p+CwSNR7vOkw0FNc= 14 | app: adopt-a-hydrant 15 | on: 16 | repo: codeforamerica/adopt-a-hydrant 17 | provider: heroku 18 | strategy: git 19 | -------------------------------------------------------------------------------- /app/controllers/reminders_controller.rb: -------------------------------------------------------------------------------- 1 | class RemindersController < ApplicationController 2 | respond_to :json 3 | 4 | def create 5 | @reminder = Reminder.new(reminder_params) 6 | @reminder.from_user = current_user 7 | if @reminder.save 8 | ThingMailer.reminder(@reminder.thing).deliver_now 9 | @reminder.update_attribute(:sent, true) 10 | render(json: @reminder) 11 | else 12 | render(json: {errors: @reminder.errors}, status: 500) 13 | end 14 | end 15 | 16 | private 17 | 18 | def reminder_params 19 | params.require(:reminder).permit(:thing_id, :to_user_id) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/views/layouts/_flash.html.haml: -------------------------------------------------------------------------------- 1 | - if flash[:notice] 2 | .alert.alert-info.fade.in{:"data-alert" => true} 3 | %a{:class => "close", :"data-dismiss" => "alert", :href => "#"} 4 | × 5 | %p 6 | = flash[:notice] 7 | - if flash[:warning] 8 | .alert.fade.in{:"data-alert" => true} 9 | %a{:class => "close", :"data-dismiss" => "alert", :href => "#"} 10 | × 11 | %p 12 | = flash[:warning] 13 | - if flash[:error] 14 | .alert.alert-error.fade.in{:"data-alert" => true} 15 | %a{:class => "close", :"data-dismiss" => "alert", :href => "#"} 16 | × 17 | %p 18 | = flash[:error] 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore bundle 11 | /vendor/bundle 12 | 13 | # Ignore SimpleCov directory. 14 | /coverage 15 | 16 | # Ignore the default SQLite database. 17 | /db/*.sqlite3 18 | /db/*.sqlite3-journal 19 | 20 | # Ignore all logfiles and tempfiles. 21 | /log/* 22 | !/log/.keep 23 | /tmp 24 | -------------------------------------------------------------------------------- /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 any plugin's vendor/assets/javascripts directory 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_tree . 14 | -------------------------------------------------------------------------------- /test/controllers/things_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ThingsControllerTest < ActionController::TestCase 4 | setup do 5 | @thing = things(:thing_1) 6 | end 7 | 8 | test 'should list hydrants' do 9 | get :show, format: 'json', lat: 42.358431, lng: -71.059773 10 | assert_not_nil assigns :things 11 | assert_response :success 12 | end 13 | 14 | test 'should update hydrant' do 15 | assert_not_equal 'Birdsill', @thing.name 16 | put :update, format: 'json', id: @thing.id, thing: {name: 'Birdsill'} 17 | @thing.reload 18 | assert_equal 'Birdsill', @thing.name 19 | assert_not_nil assigns :thing 20 | assert_response :success 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | devise_for :users, controllers: { 3 | passwords: 'passwords', 4 | registrations: 'users', 5 | sessions: 'sessions', 6 | } 7 | 8 | get '/address', to: 'addresses#show', as: 'address' 9 | get '/info_window', to: 'info_window#index', as: 'info_window' 10 | get '/sitemap', to: 'sitemaps#index', as: 'sitemap' 11 | 12 | scope '/sidebar', controller: :sidebar do 13 | get :search, as: 'search' 14 | get :combo_form, as: 'combo_form' 15 | get :edit_profile, as: 'edit_profile' 16 | end 17 | 18 | resource :reminders 19 | resource :things 20 | mount RailsAdmin::Engine => '/admin', :as => 'rails_admin' 21 | root to: 'main#index' 22 | end 23 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | ruby '2.3.1' 3 | 4 | gem 'rails', '~> 4.2.7' 5 | 6 | gem 'arel' 7 | gem 'devise' 8 | gem 'geokit' 9 | gem 'haml' 10 | gem 'http_accept_language' 11 | gem 'nokogiri' 12 | gem 'pg' 13 | gem 'rails_12factor' 14 | gem 'rails_admin' 15 | gem 'validates_formatting_of' 16 | 17 | platforms :ruby_18 do 18 | gem 'fastercsv' 19 | end 20 | 21 | group :assets do 22 | gem 'sass-rails', '>= 4.0.3' 23 | gem 'uglifier' 24 | end 25 | 26 | group :development do 27 | gem 'spring' 28 | end 29 | 30 | group :production do 31 | gem 'puma' 32 | gem 'skylight' 33 | end 34 | 35 | group :test do 36 | gem 'coveralls', require: false 37 | gem 'rubocop' 38 | gem 'simplecov', require: false 39 | gem 'sqlite3' 40 | gem 'webmock' 41 | end 42 | -------------------------------------------------------------------------------- /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 any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any styles 10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new 11 | * file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /app/controllers/things_controller.rb: -------------------------------------------------------------------------------- 1 | class ThingsController < ApplicationController 2 | respond_to :json 3 | 4 | def show 5 | @things = Thing.find_closest(params[:lat], params[:lng], params[:limit] || 10) 6 | if @things.blank? 7 | render(json: {errors: {address: [t('errors.not_found', thing: t('defaults.thing'))]}}, status: 404) 8 | else 9 | respond_with @things 10 | end 11 | end 12 | 13 | def update 14 | @thing = Thing.find(params[:id]) 15 | if @thing.update_attributes(thing_params) 16 | respond_with @thing 17 | else 18 | render(json: {errors: @thing.errors}, status: 500) 19 | end 20 | end 21 | 22 | private 23 | 24 | def thing_params 25 | params.require(:thing).permit(:name, :user_id) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | # Prevent CSRF attacks by raising an exception. 3 | # For APIs, you may want to use :null_session instead. 4 | protect_from_forgery with: :exception 5 | before_action :set_flash_from_params 6 | before_action :set_locale 7 | 8 | protected 9 | 10 | def set_flash_from_params 11 | params.fetch(:flash, []).each do |key, message| 12 | flash.now[key.to_sym] = message 13 | end 14 | end 15 | 16 | def set_locale 17 | available_languages = Dir.glob(Rails.root + 'config/locales/??.yml').collect do |file| 18 | File.basename(file, '.yml') 19 | end 20 | I18n.locale = http_accept_language.compatible_language_from(available_languages) || I18n.default_locale 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/fixtures/things.yml: -------------------------------------------------------------------------------- 1 | --- 2 | thing_1: 3 | city_id: 1 4 | lat: 42.383339 5 | lng: -71.049226 6 | 7 | thing_2: 8 | city_id: 2, 9 | lat: 42.381021 10 | lng: -71.075964 11 | 12 | thing_3: 13 | city_id: 3 14 | lat: 42.380106 15 | lng: -71.073419 16 | 17 | thing_4: 18 | city_id: 4 19 | lat: 42.377728 20 | lng: -71.070918 21 | 22 | thing_5: 23 | city_id: 5 24 | lat: 42.377281 25 | lng: -71.071576 26 | 27 | thing_6: 28 | city_id: 6 29 | lat: 42.375331 30 | lng: -71.065169 31 | 32 | thing_7: 33 | city_id: 7 34 | lat: 42.373212 35 | lng: -71.056064 36 | 37 | thing_8: 38 | city_id: 8 39 | lat: 42.374992 40 | lng: -71.038888 41 | 42 | thing_9: 43 | city_id: 9 44 | lat: 42.345635 45 | lng: -71.139308 46 | 47 | thing_10: 48 | city_id: 10 49 | lat: 42.371378 50 | lng: -71.038005 51 | -------------------------------------------------------------------------------- /app/controllers/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class SessionsController < Devise::SessionsController 2 | skip_before_action :verify_authenticity_token, only: [:destroy] 3 | 4 | def new 5 | redirect_to(root_path) 6 | end 7 | 8 | def create 9 | self.resource = warden.authenticate!(auth_options) 10 | if resource 11 | sign_in(resource_name, resource) 12 | yield resource if block_given? 13 | render(json: resource) 14 | else 15 | render(json: {errors: {password: [t('errors.password')]}}, status: 401) 16 | end 17 | end 18 | 19 | def destroy 20 | signed_in = signed_in?(resource_name) 21 | sign_out(resource_name) if signed_in 22 | Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name) 23 | yield resource if block_given? 24 | render(json: {success: signed_in}) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] ||= 'test' 2 | 3 | require 'simplecov' 4 | require 'coveralls' 5 | 6 | SimpleCov.formatters = [SimpleCov::Formatter::HTMLFormatter, Coveralls::SimpleCov::Formatter] 7 | SimpleCov.start('rails') do 8 | minimum_coverage(96.11) 9 | end 10 | 11 | require File.expand_path('../../config/environment', __FILE__) 12 | require 'rails/test_help' 13 | require 'webmock/minitest' 14 | 15 | module ActiveSupport 16 | class TestCase 17 | ActiveRecord::Migration.check_pending! 18 | 19 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 20 | # 21 | # Note: You'll currently still have to declare fixtures explicitly in integration tests 22 | # -- they do not yet inherit this setting 23 | fixtures :all 24 | 25 | # Add more helper methods to be used by all tests here... 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | 4 | # path to your application root. 5 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 6 | 7 | Dir.chdir APP_ROOT do 8 | # This script is a starting point to setup your application. 9 | # Add necessary setup steps to this file: 10 | 11 | puts "== Installing dependencies ==" 12 | system "gem install bundler --conservative" 13 | system "bundle check || bundle install" 14 | 15 | # puts "\n== Copying sample files ==" 16 | # unless File.exist?("config/database.yml") 17 | # system "cp config/database.yml.sample config/database.yml" 18 | # end 19 | 20 | puts "\n== Preparing database ==" 21 | system "bin/rake db:setup" 22 | 23 | puts "\n== Removing old logs and tempfiles ==" 24 | system "rm -f log/*" 25 | system "rm -rf tmp/cache" 26 | 27 | puts "\n== Restarting application server ==" 28 | system "touch tmp/restart.txt" 29 | end 30 | -------------------------------------------------------------------------------- /config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | if Rails.env.production? && ENV['SECRET_TOKEN'].blank? 11 | raise 'The SECRET_TOKEN environment variable is not set.\n 12 | To generate it, run "rake secret", then set it with "heroku config:set SECRET_TOKEN=the_token_you_generated"' 13 | end 14 | 15 | # Make sure your secret_key_base is kept private 16 | # if you're sharing your code publicly. 17 | AdoptAThing::Application.config.secret_key_base = ENV['SECRET_TOKEN'] || 'cfc501e00aeb29750826f86459cccec45ea2c7dd84e8fc0b800dced308be95059b51c3402d215d267cfc09f03bd6f1f531a65456212b3531ef2b10cf605dc39a' 18 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | include ActiveModel::ForbiddenAttributesProtection 3 | devise :database_authenticatable, :registerable, :recoverable, :rememberable, 4 | :trackable, :validatable 5 | before_validation :remove_non_digits_from_phone_numbers 6 | has_many :reminders_from, class_name: 'Reminder', foreign_key: 'from_user_id' 7 | has_many :reminders_to, class_name: 'Reminder', foreign_key: 'to_user_id' 8 | has_many :things 9 | validates :name, presence: true 10 | validates_formatting_of :email, using: :email 11 | validates_formatting_of :sms_number, using: :us_phone, allow_blank: true 12 | validates_formatting_of :voice_number, using: :us_phone, allow_blank: true 13 | validates_formatting_of :zip, using: :us_zip, allow_blank: true 14 | 15 | def remove_non_digits_from_phone_numbers 16 | self.sms_number = sms_number.to_s.gsub(/\D/, '').to_i if sms_number.present? 17 | self.voice_number = voice_number.to_s.gsub(/\D/, '').to_i if voice_number.present? 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/views/main/unauthenticated.html.haml: -------------------------------------------------------------------------------- 1 | .table 2 | .table-row 3 | .table-cell.sidebar 4 | %h1 5 | = image_tag "logos/adopt-a-hydrant.png", :alt => t("titles.main", :thing => t("defaults.thing").titleize), :title => t("titles.main", :thing => t("defaults.thing").titleize) 6 | %p.alert-message.block-message#tagline 7 | = t("defaults.tagline") 8 | #content 9 | = render :partial => "sidebar/combo_form" 10 | #logos 11 | %a{:href => "http://codeforamerica.org/"} 12 | = image_tag "logos/cfa.png", :alt => t("sponsors.cfa"), :title => t("sponsors.cfa") 13 | %a{:href => "http://bostonbuilt.org/"} 14 | = image_tag "logos/boston.png", :alt => t("sponsors.city"), :title => t("sponsors.city") 15 | #feedback 16 | %a{:href => URI.escape("mailto:adoptahydrant@cityofboston.gov?subject=#{t("titles.main", :thing => t("defaults.thing").titleize)} #{t("links.feedback").titleize}")} 17 | = t("links.feedback") 18 | .table-cell#map 19 |   20 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: 0e16faa4e63cf660f77d2b01b3eea1cf05d92cc2550dd5c6bcdd6c428cacd9ed82cf420376130716e8ce890490cfdb9d30241f12d0043ef1c2356e0ee22c031b 15 | 16 | test: 17 | secret_key_base: de935ed2ec52cc94165a0542fd3fc2aefc5eb56557b31d6333adb15fe413a2ae89b02ffd402b7fc2279dcd9fbcb45f6c4c764937665f0c61abbbf2bf5c70e967 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /app/models/thing.rb: -------------------------------------------------------------------------------- 1 | require 'geokit' 2 | 3 | class Thing < ActiveRecord::Base 4 | extend Forwardable 5 | include ActiveModel::ForbiddenAttributesProtection 6 | belongs_to :user 7 | def_delegators :reverse_geocode, :city, :country, :country_code, 8 | :full_address, :state, :street_address, :street_name, 9 | :street_number, :zip 10 | has_many :reminders 11 | validates :city_id, uniqueness: true, allow_nil: true 12 | validates :lat, presence: true 13 | validates :lng, presence: true 14 | 15 | def self.find_closest(lat, lng, limit = 10) 16 | query = <<-SQL 17 | SELECT *, (3959 * ACOS(COS(RADIANS(?)) * COS(RADIANS(lat)) * COS(RADIANS(lng) - RADIANS(?)) + SIN(RADIANS(?)) * SIN(RADIANS(lat)))) AS distance 18 | FROM things 19 | ORDER BY distance 20 | LIMIT ? 21 | SQL 22 | find_by_sql([query, lat.to_f, lng.to_f, lat.to_f, limit.to_i]) 23 | end 24 | 25 | def reverse_geocode 26 | @reverse_geocode ||= Geokit::Geocoders::MultiGeocoder.reverse_geocode([lat, lng]) 27 | end 28 | 29 | def adopted? 30 | !user.nil? 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/controllers/passwords_controller.rb: -------------------------------------------------------------------------------- 1 | class PasswordsController < Devise::PasswordsController 2 | def create 3 | self.resource = resource_class.send_reset_password_instructions(resource_params) 4 | yield resource if block_given? 5 | if successfully_sent?(resource) 6 | render(json: {success: true}) 7 | else 8 | render(json: {errors: resource.errors}, status: 500) 9 | end 10 | end 11 | 12 | def edit 13 | self.resource = resource_class.new 14 | resource.reset_password_token = params[:reset_password_token] 15 | render('edit', layout: 'info_window') 16 | end 17 | 18 | def update 19 | self.resource = resource_class.reset_password_by_token(resource_params) 20 | yield resource if block_given? 21 | if resource.errors.empty? 22 | resource.unlock_access! if unlockable?(resource) 23 | sign_in(resource_name, resource) 24 | end 25 | redirect_to(controller: 'main', action: 'index') 26 | end 27 | 28 | private 29 | 30 | def resource_params 31 | params.require(:user).permit(:email, :password, :password_confirmation, :reset_password_token) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/controllers/sessions_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class SessionsControllerTest < ActionController::TestCase 4 | include Devise::TestHelpers 5 | setup do 6 | request.env['devise.mapping'] = Devise.mappings[:user] 7 | @user = users(:erik) 8 | end 9 | 10 | test 'should redirect to root path' do 11 | get :new 12 | assert_response :redirect 13 | end 14 | 15 | test 'should redirect if user is already authenticated' do 16 | sign_in @user 17 | get :new 18 | assert_response :redirect 19 | end 20 | 21 | test 'should authenticate user if password is correct' do 22 | post :create, user: {email: @user.email, password: 'correct'}, format: :json 23 | assert_response :success 24 | end 25 | 26 | test 'should return error if password is incorrect' do 27 | post :create, user: {email: @user.email, password: 'incorrect'}, format: :json 28 | assert_response 401 29 | end 30 | 31 | test 'should empty session on sign out' do 32 | sign_in @user 33 | get :destroy, format: :json 34 | assert session.empty? 35 | assert_response :success 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test/controllers/reminders_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class RemindersControllerTest < ActionController::TestCase 4 | include Devise::TestHelpers 5 | setup do 6 | request.env['devise.mapping'] = Devise.mappings[:user] 7 | @thing = things(:thing_1) 8 | @dan = users(:dan) 9 | @user = users(:erik) 10 | @thing.user = @dan 11 | @thing.save! 12 | stub_request(:get, 'https://maps.google.com/maps/api/geocode/json'). 13 | with(query: {latlng: '42.383339,-71.049226', sensor: 'false'}). 14 | to_return(body: File.read(File.expand_path('../../fixtures/city_hall.json', __FILE__))) 15 | end 16 | 17 | test 'should send a reminder email' do 18 | sign_in @user 19 | num_deliveries = ActionMailer::Base.deliveries.size 20 | post :create, format: :json, reminder: {thing_id: @thing.id, to_user_id: @dan.id} 21 | assert_equal num_deliveries + 1, ActionMailer::Base.deliveries.size 22 | assert_response :success 23 | email = ActionMailer::Base.deliveries.last 24 | assert_equal [@dan.email], email.to 25 | assert_equal 'Remember to shovel', email.subject 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/views/sidebar/_search.html.haml: -------------------------------------------------------------------------------- 1 | = form_tag "/address", :method => "get", :id => "address_form", :class => "search-form" do 2 | = hidden_field_tag "limit", params[:limit] || 10 3 | %fieldset.control-group 4 | = label_tag "city_state", t("labels.city_state"), :id => "city_state_label" 5 | = select_tag "city_state", "".html_safe 6 | %fieldset.control-group 7 | = label_tag "address", t("labels.address"), :id => "address_label" 8 | = search_field_tag "address", params[:address], :placeholder => [t("defaults.address_1"), t("defaults.neighborhood")].join(", "), :class => "search-query" 9 | %fieldset.form-actions 10 | = submit_tag t("buttons.find", :thing => t("defaults.thing").pluralize), :class => "btn btn-primary" 11 | %a{:href => edit_user_registration_path, :id => "edit_profile_link", :class => "btn"} 12 | = t("buttons.edit_profile") 13 | %a{:href => destroy_user_session_path, :id => "sign_out_link", :class => "btn btn-danger"} 14 | = t("buttons.sign_out") 15 | :javascript 16 | $(function() { 17 | $('#address').focus(); 18 | }); 19 | -------------------------------------------------------------------------------- /test/controllers/addresses_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class AddressesControllerTest < ActionController::TestCase 4 | test 'should return latitude and longitude for a valid address' do 5 | stub_request(:get, 'https://maps.google.com/maps/api/geocode/json'). 6 | with(query: {address: 'City Hall, Boston, MA', sensor: 'false'}). 7 | to_return(body: File.read(File.expand_path('../../fixtures/city_hall.json', __FILE__))) 8 | get :show, address: 'City Hall', city_state: 'Boston, MA', format: 'json' 9 | assert_not_nil assigns :address 10 | end 11 | 12 | test 'should return an error for an invalid address' do 13 | stub_request(:get, 'https://maps.google.com/maps/api/geocode/json'). 14 | with(query: {address: ', ', sensor: 'false'}). 15 | to_return(body: File.read(File.expand_path('../../fixtures/unknown_address.json', __FILE__))) 16 | stub_request(:get, 'http://geocoder.us/service/csv/geocode'). 17 | with(query: {address: ', '}). 18 | to_return(body: File.read(File.expand_path('../../fixtures/unknown_address.json', __FILE__))) 19 | get :show, address: '', city_state: '', format: 'json' 20 | assert_response :missing 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /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(*Rails.groups) 8 | 9 | module AdoptAThing 10 | class Application < Rails::Application 11 | # Settings in config/environments/* take precedence over those specified here. 12 | # Application configuration should go into files in config/initializers 13 | # -- all .rb files in that directory are automatically loaded. 14 | 15 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 16 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 17 | # config.time_zone = 'Central Time (US & Canada)' 18 | 19 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 20 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 21 | # config.i18n.default_locale = :de 22 | 23 | # Do not swallow errors in after_commit/after_rollback callbacks. 24 | config.active_record.raise_in_transactional_callbacks = true 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/controllers/main_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class MainControllerTest < ActionController::TestCase 4 | include Devise::TestHelpers 5 | 6 | setup do 7 | request.env['devise.mapping'] = Devise.mappings[:user] 8 | @user = users(:erik) 9 | end 10 | 11 | test 'should return the home page' do 12 | get :index 13 | assert_response :success 14 | assert_select 'title', 'Adopt-a-Hydrant' 15 | assert_select 'p#tagline', 'Claim responsibility for shoveling out a fire hydrant after it snows.' 16 | end 17 | 18 | test 'should show search form when signed in' do 19 | sign_in @user 20 | get :index 21 | assert_response :success 22 | assert_select 'form' do 23 | assert_select '[action=?]', '/address' 24 | assert_select '[method=?]', 'get' 25 | end 26 | assert_select 'label#city_state_label', 'City' 27 | assert_select 'select#city_state' do 28 | assert_select 'option', 'Boston, Massachusetts' 29 | end 30 | assert_select 'label#address_label', 'Address, Neighborhood' 31 | assert_select 'input#address', true 32 | assert_select 'input[name="commit"]' do 33 | assert_select '[type=?]', 'submit' 34 | assert_select '[value=?]', 'Find hydrants' 35 | end 36 | assert_select 'div#map', true 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | Exclude: 3 | - 'bin/*' 4 | - 'db/schema.rb' 5 | - 'db/seeds.rb' 6 | - 'vendor/bundle/**/*' 7 | 8 | Rails: 9 | Enabled: true 10 | 11 | Metrics/AbcSize: 12 | Exclude: 13 | - 'app/controllers/passwords_controller.rb' 14 | - 'app/controllers/sessions_controller.rb' 15 | - 'app/controllers/users_controller.rb' 16 | 17 | Metrics/BlockNesting: 18 | Max: 2 19 | 20 | Metrics/LineLength: 21 | AllowURI: true 22 | Enabled: false 23 | 24 | Metrics/MethodLength: 25 | CountComments: false 26 | Max: 10 27 | Exclude: 28 | - 'db/migrate/*.rb' 29 | 30 | Metrics/ParameterLists: 31 | Max: 4 32 | CountKeywordArgs: true 33 | 34 | Style/AccessModifierIndentation: 35 | EnforcedStyle: outdent 36 | 37 | Style/CollectionMethods: 38 | PreferredMethods: 39 | map: 'collect' 40 | reduce: 'inject' 41 | find: 'detect' 42 | find_all: 'select' 43 | 44 | Style/Documentation: 45 | Enabled: false 46 | 47 | Style/DotPosition: 48 | EnforcedStyle: trailing 49 | 50 | Style/DoubleNegation: 51 | Enabled: false 52 | 53 | Style/FrozenStringLiteralComment: 54 | Enabled: false 55 | 56 | Style/SpaceInsideHashLiteralBraces: 57 | EnforcedStyle: no_space 58 | 59 | Style/TrailingCommaInLiteral: 60 | EnforcedStyleForMultiline: 'comma' 61 | 62 | Style/WordArray: 63 | Exclude: 64 | - 'app/helpers/application_helper.rb' 65 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.haml: -------------------------------------------------------------------------------- 1 | !!! 5 2 | %html 3 | %head 4 | %title 5 | = t("titles.main", :thing => t("defaults.thing").titleize) 6 | = csrf_meta_tag 7 | %meta{:name => "viewport", :content => "width=800, user-scalable=no"} 8 | / HTML5 shim, for IE6-8 support of HTML5 elements 9 | /[if lt IE 9] 10 | = javascript_include_tag "//html5shim.googlecode.com/svn/trunk/html5.js" 11 | = javascript_include_tag "//maps.google.com/maps/api/js?sensor=false&language=#{I18n.locale}" 12 | = javascript_include_tag "//ajax.googleapis.com/ajax/libs/jquery/1.8/jquery.min.js" 13 | = javascript_include_tag "//ajax.googleapis.com/ajax/libs/jqueryui/1.8/jquery-ui.min.js" 14 | = javascript_include_tag "application" 15 | - if Rails.env.production? && ENV['GOOGLE_ANALYTICS_ID'].present? 16 | %script{:type => "text/javascript"} 17 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 18 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 19 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 20 | })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); 21 | 22 | ga('create', '#{ENV['GOOGLE_ANALYTICS_ID']}', '#{ENV['GOOGLE_ANALYTICS_DOMAIN']}'); 23 | ga('send', 'pageview'); 24 | = stylesheet_link_tag "application" 25 | %body 26 | = yield 27 | -------------------------------------------------------------------------------- /app/controllers/users_controller.rb: -------------------------------------------------------------------------------- 1 | class UsersController < Devise::RegistrationsController 2 | def edit 3 | render('sidebar/edit_profile', layout: 'sidebar') 4 | end 5 | 6 | def update 7 | self.resource = resource_class.to_adapter.get!(send(:"current_#{resource_name}").to_key) 8 | if update_resource(resource, account_update_params) 9 | yield resource if block_given? 10 | sign_in(resource_name, resource, bypass: true) 11 | flash[:notice] = 'Profile updated!' 12 | redirect_to(controller: 'sidebar', action: 'search') 13 | else 14 | clean_up_passwords(resource) 15 | render(json: {errors: resource.errors}, status: 500) 16 | end 17 | end 18 | 19 | def create 20 | build_resource(sign_up_params) 21 | if resource.save 22 | yield resource if block_given? 23 | sign_in(resource_name, resource) 24 | render(json: resource) 25 | else 26 | clean_up_passwords(resource) 27 | render(json: {errors: resource.errors}, status: 500) 28 | end 29 | end 30 | 31 | private 32 | 33 | def sign_up_params 34 | params.require(:user).permit(:email, :name, :organization, :password, :password_confirmation, :sms_number, :voice_number) 35 | end 36 | 37 | def account_update_params 38 | params.require(:user).permit(:address_1, :address_2, :city, :current_password, :email, :name, :organization, :password, :password_confirmation, :remember_me, :sms_number, :state, :voice_number, :zip) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Code for America. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | * Neither the name of Code for America nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 9 | 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | -------------------------------------------------------------------------------- /app/views/main/index.html.haml: -------------------------------------------------------------------------------- 1 | .table 2 | .table-row 3 | .table-cell.sidebar 4 | %h1 5 | = image_tag "logos/adopt-a-hydrant.png", :alt => t("titles.main", :thing => t("defaults.thing").titleize), :title => t("titles.main", :thing => t("defaults.thing").titleize) 6 | %p.alert-message.block-message#tagline 7 | = t("defaults.tagline") 8 | #content 9 | = render :partial => "layouts/flash", :locals => {:flash => flash} 10 | - if signed_in? 11 | = render :partial => "sidebar/search" 12 | - else 13 | = render :partial => "sidebar/combo_form" 14 | #logos 15 | #col1 16 | %a{:href => "http://codeforamerica.org/"} 17 | = image_tag "logos/cfa.png", :alt => t("sponsors.cfa"), :title => t("sponsors.cfa") 18 | %br 19 | %a{:href => "http://bostonbuilt.org/"} 20 | = image_tag "logos/bostonbuilt.png", :alt => t("sponsors.built"), :title => t("sponsors.built") 21 | #col2 22 | %a{:href => "http://www.cityofboston.gov/"} 23 | = image_tag "logos/boston.png", :alt => t("sponsors.city"), :title => t("sponsors.city") 24 | %p#mayor 25 | %a{:href => "http://www.cityofboston.gov/mayor/"} 26 | = "#{t("sponsors.mayor.name")}," 27 | %em 28 | = t("sponsors.mayor.title") 29 | #feedback 30 | %p 31 | %a{:href => URI.escape("mailto:adoptahydrant@cityofboston.gov?subject=#{t("titles.main", :thing => t("defaults.thing").titleize)} - #{t("links.feedback").titleize}")} 32 | = t("links.feedback") 33 | .table-cell.map-container 34 | #map 35 |   36 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | def us_states # rubocop:disable MethodLength 3 | [ 4 | ['Massachusetts', 'MA'], 5 | ['Alabama', 'AL'], 6 | ['Alaska', 'AK'], 7 | ['Arizona', 'AZ'], 8 | ['Arkansas', 'AR'], 9 | ['California', 'CA'], 10 | ['Colorado', 'CO'], 11 | ['Connecticut', 'CT'], 12 | ['Delaware', 'DE'], 13 | ['District of Columbia', 'DC'], 14 | ['Florida', 'FL'], 15 | ['Georgia', 'GA'], 16 | ['Hawaii', 'HI'], 17 | ['Idaho', 'ID'], 18 | ['Illinois', 'IL'], 19 | ['Indiana', 'IN'], 20 | ['Iowa', 'IA'], 21 | ['Kansas', 'KS'], 22 | ['Kentucky', 'KY'], 23 | ['Louisiana', 'LA'], 24 | ['Maine', 'ME'], 25 | ['Maryland', 'MD'], 26 | ['Massachusetts', 'MA'], 27 | ['Michigan', 'MI'], 28 | ['Minnesota', 'MN'], 29 | ['Mississippi', 'MS'], 30 | ['Missouri', 'MO'], 31 | ['Montana', 'MT'], 32 | ['Nebraska', 'NE'], 33 | ['Nevada', 'NV'], 34 | ['New Hampshire', 'NH'], 35 | ['New Jersey', 'NJ'], 36 | ['New Mexico', 'NM'], 37 | ['New York', 'NY'], 38 | ['North Carolina', 'NC'], 39 | ['North Dakota', 'ND'], 40 | ['Ohio', 'OH'], 41 | ['Oklahoma', 'OK'], 42 | ['Oregon', 'OR'], 43 | ['Pennsylvania', 'PA'], 44 | ['Puerto Rico', 'PR'], 45 | ['Rhode Island', 'RI'], 46 | ['South Carolina', 'SC'], 47 | ['South Dakota', 'SD'], 48 | ['Tennessee', 'TN'], 49 | ['Texas', 'TX'], 50 | ['Utah', 'UT'], 51 | ['Vermont', 'VT'], 52 | ['Virginia', 'VA'], 53 | ['Washington', 'WA'], 54 | ['West Virginia', 'WV'], 55 | ['Wisconsin', 'WI'], 56 | ['Wyoming', 'WY'], 57 | ] 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | config.action_mailer.raise_delivery_errors = false 18 | config.action_mailer.default_url_options = {host: 'localhost:3000'} 19 | 20 | # Print deprecation notices to the Rails logger. 21 | config.active_support.deprecation = :log 22 | 23 | # Raise an error on page load if there are pending migrations. 24 | config.active_record.migration_error = :page_load 25 | 26 | # Debug mode disables concatenation and preprocessing of assets. 27 | # This option may cause significant delays in view rendering with a large 28 | # number of complex assets. 29 | config.assets.debug = true 30 | 31 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 32 | # yet still be able to expire them through the digest params. 33 | config.assets.digest = true 34 | 35 | # Adds additional error checking when serving assets at runtime. 36 | # Checks for improperly declared sprockets dependencies. 37 | # Raises helpful error messages. 38 | config.assets.raise_runtime_errors = true 39 | 40 | # Raises error for missing translations 41 | # config.action_view.raise_on_missing_translations = true 42 | end 43 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/controllers/users_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UsersControllerTest < ActionController::TestCase 4 | include Devise::TestHelpers 5 | setup do 6 | request.env['devise.mapping'] = Devise.mappings[:user] 7 | @user = users(:erik) 8 | end 9 | 10 | test 'should render edit form' do 11 | sign_in @user 12 | get :edit 13 | assert_response :success 14 | assert_select 'form#edit_form' do 15 | assert_select '[action=?]', '/users' 16 | assert_select '[method=?]', 'post' 17 | end 18 | assert_select 'input', count: 15 19 | assert_select 'label', count: 12 20 | assert_select 'input[name="commit"]' do 21 | assert_select '[type=?]', 'submit' 22 | assert_select '[value=?]', 'Update' 23 | end 24 | assert_select 'a.btn', 'Back' 25 | end 26 | 27 | test 'should update user if password is correct' do 28 | sign_in @user 29 | assert_not_equal 'New Name', @user.name 30 | put :update, user: {name: 'New Name', current_password: 'correct'} 31 | @user.reload 32 | assert_equal 'New Name', @user.name 33 | assert_response :redirect 34 | assert_redirected_to controller: 'sidebar', action: 'search' 35 | end 36 | 37 | test 'should return error if password is incorrect' do 38 | sign_in @user 39 | put :update, user: {name: 'New Name', current_password: 'incorrect'} 40 | assert_response :error 41 | end 42 | 43 | test 'should create user if information is valid' do 44 | post :create, user: {email: 'user@example.com', name: 'User', password: 'correct', password_confirmation: 'correct'} 45 | assert_response :success 46 | end 47 | 48 | test 'should return error if information is invalid' do 49 | post :create, user: {email: 'user@example.com', name: 'User', password: 'correct', password_confirmation: 'incorrect'} 50 | assert_response :error 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /db/migrate/00000000000002_add_devise_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddDeviseToUsers < ActiveRecord::Migration 2 | def up 3 | change_table(:users) do |t| 4 | ## Database authenticatable 5 | # t.string :email, null: false, default: "" 6 | t.string :encrypted_password, null: false, default: '' 7 | 8 | ## Recoverable 9 | t.string :reset_password_token 10 | t.datetime :reset_password_sent_at 11 | 12 | ## Rememberable 13 | t.datetime :remember_created_at 14 | 15 | ## Trackable 16 | t.integer :sign_in_count, default: 0, null: false 17 | t.datetime :current_sign_in_at 18 | t.datetime :last_sign_in_at 19 | t.string :current_sign_in_ip 20 | t.string :last_sign_in_ip 21 | 22 | ## Confirmable 23 | # t.string :confirmation_token 24 | # t.datetime :confirmed_at 25 | # t.datetime :confirmation_sent_at 26 | # t.string :unconfirmed_email # Only if using reconfirmable 27 | 28 | ## Lockable 29 | # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts 30 | # t.string :unlock_token # Only if unlock strategy is :email or :both 31 | # t.datetime :locked_at 32 | 33 | # Uncomment below if timestamps were not included in your original model. 34 | # t.timestamps 35 | end 36 | 37 | # add_index :users, :email, unique: true 38 | add_index :users, :reset_password_token, unique: true 39 | # add_index :users, :confirmation_token, unique: true 40 | # add_index :users, :unlock_token, unique: true 41 | end 42 | 43 | def down 44 | # By default, we don't want to make any assumption about how to roll back a migration when your 45 | # model already existed. Please edit below which fields you would like to remove in this migration. 46 | raise ActiveRecord::IrreversibleMigration 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /test/controllers/passwords_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class PasswordsControllerTest < ActionController::TestCase 4 | include Devise::TestHelpers 5 | setup do 6 | request.env['devise.mapping'] = Devise.mappings[:user] 7 | @user = users(:erik) 8 | end 9 | 10 | test 'should send password reset instructions if email address is found' do 11 | num_deliveries = ActionMailer::Base.deliveries.size 12 | post :create, user: {email: @user.email} 13 | assert_equal num_deliveries + 1, ActionMailer::Base.deliveries.size 14 | assert_response :success 15 | email = ActionMailer::Base.deliveries.last 16 | assert_equal [@user.email], email.to 17 | assert_equal 'Reset password instructions', email.subject 18 | end 19 | 20 | test 'should not send password reset instructions if email address is not found' do 21 | post :create, user: {email: 'not_found@example.com'} 22 | assert_response :error 23 | end 24 | 25 | test 'should render edit view' do 26 | get :edit, reset_password_token: 'token' 27 | assert_response :success 28 | end 29 | 30 | test 'should reset user password with an valid reset password token' do 31 | token = @user.send_reset_password_instructions 32 | put :update, user: {reset_password_token: token, password: 'new_password'} 33 | @user.reload 34 | assert @user.valid_password?('new_password') 35 | assert_response :redirect 36 | assert_redirected_to controller: 'main', action: 'index' 37 | end 38 | 39 | test 'should not reset user password with an invalid reset password token' do 40 | @user.send_reset_password_instructions 41 | put :update, user: {reset_password_token: 'invalid_token', password: 'new_password'} 42 | @user.reload 43 | assert !@user.valid_password?('new_password') 44 | assert_response :redirect 45 | assert_redirected_to controller: 'main', action: 'index' 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static file server for tests with Cache-Control for performance. 16 | config.serve_static_files = true 17 | config.static_cache_control = 'public, max-age=3600' 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | config.log_level = :warn 24 | 25 | # Raise exceptions instead of rendering exception templates. 26 | config.action_dispatch.show_exceptions = false 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | 31 | # Tell Action Mailer not to deliver emails to the real world. 32 | # The :test delivery method accumulates sent emails in the 33 | # ActionMailer::Base.deliveries array. 34 | config.action_mailer.delivery_method = :test 35 | config.action_mailer.default_url_options = {host: 'localhost:3000'} 36 | 37 | # Randomize the order test cases are executed. 38 | config.active_support.test_order = :random 39 | 40 | # Print deprecation notices to the stderr. 41 | config.active_support.deprecation = :stderr 42 | 43 | # Raises error for missing translations 44 | # config.action_view.raise_on_missing_translations = true 45 | end 46 | -------------------------------------------------------------------------------- /test/controllers/info_window_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class InfoWindowControllerTest < ActionController::TestCase 4 | include Devise::TestHelpers 5 | setup do 6 | @thing = things(:thing_1) 7 | @user = users(:erik) 8 | end 9 | 10 | test 'should thank the user if the hydrant is adopted by the user' do 11 | sign_in @user 12 | @thing.user_id = @user.id 13 | @thing.save! 14 | get :index, thing_id: @thing.id 15 | assert_not_nil assigns :thing 16 | assert_response :success 17 | assert_template 'users/thank_you' 18 | assert_select 'h2', 'Thank you for adopting this hydrant!' 19 | assert_select 'form#abandon_form' do 20 | assert_select '[action=?]', '/things' 21 | assert_select '[method=?]', 'post' 22 | end 23 | assert_select 'input[name="_method"]' do 24 | assert_select '[type=?]', 'hidden' 25 | assert_select '[value=?]', 'put' 26 | end 27 | assert_select 'input[name="commit"]' do 28 | assert_select '[type=?]', 'submit' 29 | assert_select '[value=?]', 'Abandon this hydrant' 30 | end 31 | end 32 | 33 | test 'should show the profile if the hydrant is adopted' do 34 | @thing.user_id = @user.id 35 | @thing.save! 36 | get :index, thing_id: @thing.id 37 | assert_not_nil assigns :thing 38 | assert_response :success 39 | assert_template 'users/profile' 40 | assert_select 'h2', /This hydrant has been adopted\s+by #{@user.name}\s+of #{@user.organization}/ 41 | end 42 | 43 | test 'should show adoption form if hydrant is not adopted' do 44 | sign_in @user 45 | get :index, thing_id: @thing.id 46 | assert_not_nil assigns :thing 47 | assert_response :success 48 | assert_template :adopt 49 | assert_select 'h2', 'Adopt this Hydrant' 50 | assert_select 'form#adoption_form' do 51 | assert_select '[action=?]', '/things' 52 | assert_select '[method=?]', 'post' 53 | end 54 | assert_select 'input[name="_method"]' do 55 | assert_select '[type=?]', 'hidden' 56 | assert_select '[value=?]', 'put' 57 | end 58 | assert_select 'input[name="commit"]' do 59 | assert_select '[type=?]', 'submit' 60 | assert_select '[value=?]', 'Adopt!' 61 | end 62 | end 63 | 64 | test 'should show sign-in form if signed out' do 65 | get :index, thing_id: @thing.id 66 | assert_not_nil assigns :thing 67 | assert_response :success 68 | assert_template 'users/sign_in' 69 | assert_select 'h2', 'Sign in to adopt this Hydrant' 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Please maintain alphabetical order 2 | 3 | en: 4 | buttons: 5 | abandon: "Abandon this %{thing}" 6 | adopt: "Adopt!" 7 | back: "Back" 8 | change_password: "Change my password" 9 | close: "Close" 10 | edit_profile: "Edit profile" 11 | email_password: "Email me my password" 12 | find: "Find %{thing}" 13 | send_reminder: "Send reminder to shovel" 14 | sign_in: "Sign in" 15 | sign_out: "Sign out" 16 | sign_up: "Sign up" 17 | update: "Update" 18 | captions: 19 | optional: "(optional)" 20 | private: "(private)" 21 | public: "(visible to others)" 22 | required: "(required)" 23 | defaults: 24 | address: "address" 25 | address_1: "1 City Hall Plaza" 26 | address_2: "Suite 500" 27 | city: "Boston" 28 | city_state: "Boston, Massachusetts" 29 | neighborhood: "Downtown" 30 | sms_number: "857-555-1212" 31 | state: "MA" 32 | thing: "hydrant" 33 | this_thing: "This %{thing}" 34 | tagline: "Claim responsibility for shoveling out a fire hydrant after it snows." 35 | tos: "By signing up, you agree to the %{tos}." 36 | voice_number: "617-555-1212" 37 | zip: "02201-2013" 38 | errors: 39 | password: "You need to sign in or sign up before continuing." 40 | not_found: "Could not find %{thing}." 41 | labels: 42 | address: "Address, Neighborhood" 43 | address_1: "Address Line 1" 44 | address_2: "Address Line 2" 45 | city: "City" 46 | city_state: "City" 47 | email: "Email address" 48 | name: "Name" 49 | name_thing: "Give this %{thing} a name" 50 | organization: "Organization" 51 | password: "Password" 52 | password_choose: "Choose a password" 53 | password_current: "Current password" 54 | password_new: "New password" 55 | remember_me: "Stay signed in" 56 | sms_number: "Mobile phone number" 57 | state: "State" 58 | user_existing: "I've already signed up" 59 | user_new: "I haven't signed up yet" 60 | voice_number: "Home phone number" 61 | zip: "ZIP" 62 | links: 63 | feedback: "Send feedback" 64 | forgot_password: "Forgot your password?" 65 | remembered_password: "Never mind. I remembered my password." 66 | notices: 67 | abandoned: "%{thing} abandoned!" 68 | adopted: "You just adopted a %{thing}!" 69 | password_reset: "Password reset instructions sent! Check your email." 70 | reminder_sent: "Reminder sent!" 71 | signed_in: "Signed in!" 72 | signed_out: "Signed out." 73 | signed_up: "Thanks for signing up!" 74 | stolen: "%{thing} stolen!" 75 | titles: 76 | adopt: "Adopt this %{thing}" 77 | adopted: "%{thing_name} has been adopted" 78 | byline: "by %{name}" 79 | edit_profile: "Edit your Profile" 80 | main: "Adopt-a-%{thing}" 81 | ofline: "of %{organization}" 82 | sign_in: "Sign in to adopt this %{thing}" 83 | thank_you: "Thank you for adopting this %{thing}!" 84 | tos: "Terms of Service" 85 | sponsors: 86 | built: "Built in Boston" 87 | cfa: "Code for America" 88 | city: "City of Boston" 89 | mayor: 90 | name: "Martin J. Walsh" 91 | title: "Mayor" 92 | -------------------------------------------------------------------------------- /config/locales/pt.yml: -------------------------------------------------------------------------------- 1 | # Please maintain alphabetical order 2 | 3 | pt: 4 | buttons: 5 | abandon: "Abandonar esse %{thing}" 6 | adopt: "Adote!" 7 | back: "De volta" 8 | change_password: "Alterar a minha senha" 9 | close: "Fechar" 10 | edit_profile: "Editar perfil" 11 | email_password: "E-mail minha senha" 12 | find: "Encontrar %{thing}" 13 | send_reminder: "Enviar lembrete para pá" 14 | sign_in: "Entrar" 15 | sign_out: "Sair" 16 | sign_up: "Inscrever-se" 17 | update: "Atualizar" 18 | captions: 19 | optional: "(opcional)" 20 | private: "(privado)" 21 | public: "(visível para os outros)" 22 | required: "(exigido)" 23 | defaults: 24 | address: "endereço" 25 | address_1: "1 City Hall Plaza" 26 | address_2: "Suite 500" 27 | city: "Boston" 28 | city_state: "Boston, Massachusetts" 29 | neighborhood: "Downtown" 30 | sms_number: "857-555-1212" 31 | state: "MA" 32 | thing: "hidrante" 33 | this_thing: "Este %{thing}" 34 | tagline: "Responsabilidade pedido de pá para fora um %{thing} de incêndio depois que neva." 35 | tos: "Ao inscrever-se, você concorda com os %{tos}." 36 | voice_number: "617-555-1212" 37 | zip: "02201-2013" 38 | errors: 39 | password: "Você precisa fazer login ou inscreva-se antes de continuar." 40 | not_found: "Não foi possível encontrar %{thing}." 41 | labels: 42 | address: "Endereço, Bairro" 43 | address_1: "Endereço linha 1" 44 | address_2: "Endereço linha 2" 45 | city: "Cidade" 46 | city_state: "Cidade" 47 | email: "Endereço de email" 48 | name: "Nome" 49 | name_thing: "Dê este %{thing} um nome" 50 | organization: "Organização" 51 | password: "Senha" 52 | password_choose: "Escolha uma senha" 53 | password_current: "Senha atual" 54 | password_new: "Nova senha" 55 | remember_me: "Fique assinado em" 56 | sms_number: "Número de telemóvel" 57 | state: "Estado" 58 | user_existing: "Eu já se inscreveram" 59 | user_new: "Eu não se inscreveram ainda" 60 | voice_number: "Número de telefone residencial" 61 | zip: "Código postal" 62 | links: 63 | feedback: "Envie seu comentário" 64 | forgot_password: "Esqueceu sua senha?" 65 | remembered_password: "Não se preocupe. Lembrei-me minha senha." 66 | notices: 67 | abandoned: "%{thing} abandonada!" 68 | adopted: "Você acaba de aprovar um %{thing}!" 69 | password_reset: "Instruções de redefinição de senha enviada! Verifique se o seu e-mail." 70 | reminder_sent: "Lembrete enviado!" 71 | signed_in: "Assinado em!" 72 | signed_out: "Assinado para fora." 73 | signed_up: "Obrigado por se inscrever!" 74 | stolen: "%{thing} roubado!" 75 | titles: 76 | adopt: "Adotar esse %{thing}" 77 | adopted: "%{thing_name} foi adotada" 78 | byline: "por %{name}" 79 | edit_profile: "Editar seu Perfil" 80 | main: "Adotar-um-%{thing}" 81 | ofline: "do %{organization}" 82 | sign_in: "Entrar para adotar essa %{thing}" 83 | thank_you: "Obrigado por adotar este %{thing}!" 84 | tos: "Termos de Serviço" 85 | sponsors: 86 | built: "Construído em Boston" 87 | cfa: "Código para a América" 88 | city: "Cidade de Boston" 89 | mayor: 90 | name: "Martin J. Walsh" 91 | title: "Mayor" 92 | -------------------------------------------------------------------------------- /config/locales/it.yml: -------------------------------------------------------------------------------- 1 | # Please maintain alphabetical order 2 | 3 | it: 4 | buttons: 5 | abandon: "Lascia questo %{thing}" 6 | adopt: "Usalo!" 7 | back: "Indietro" 8 | change_password: "Cambia la mia password" 9 | close: "Chiudi" 10 | edit_profile: "Modifica profilo" 11 | email_password: "Mandami per Email la mia password" 12 | find: "Trova %{thing}" 13 | send_reminder: "Invia un promemoria per spalare" 14 | sign_in: "Accedi" 15 | sign_out: "Esci" 16 | sign_up: "Iscriviti" 17 | update: "Aggiorna" 18 | captions: 19 | optional: "(opzionale)" 20 | private: "(privato)" 21 | public: "(visibile ad altri)" 22 | required: "(richiesto)" 23 | defaults: 24 | address: "indirizzo" 25 | address_1: "1 City Hall Plaza" 26 | address_2: "Suite 500" 27 | city: "Boston" 28 | city_state: "Boston, Massachusetts" 29 | neighborhood: "Downtown" 30 | sms_number: "857-555-1212" 31 | state: "MA" 32 | thing: "idrante" 33 | this_thing: "Questo %{thing}" 34 | tagline: "Richiedi la responsabilità per spalare un idrante dopo una nevicata." 35 | tos: "Iscrivendoti, aderisci alle %{tos}." 36 | voice_number: "617-555-1212" 37 | zip: "02201-2013" 38 | errors: 39 | password: "E' necessario che acceda od esca prima di continuare." 40 | not_found: "Impossibile trovare %{thing}." 41 | labels: 42 | address: "Indirizzo, quartiere" 43 | address_1: "Indirizzo linea 1" 44 | address_2: "Indirizzo linea 2" 45 | city: "Città" 46 | city_state: "Città" 47 | email: "Indirizzo Email" 48 | name: "Nome" 49 | name_thing: "Dai a questo %{thing} un nome" 50 | organization: "Organizzazione" 51 | password: "Password" 52 | password_choose: "Scegli una password" 53 | password_current: "Password corrente" 54 | password_new: "Nuova password" 55 | remember_me: "Rimani connesso" 56 | sms_number: "Numero di cellulare" 57 | state: "Stato" 58 | user_existing: "Sono già registrato" 59 | user_new: "Non sono ancora registrato" 60 | voice_number: "Telefono di casa" 61 | zip: "CAP" 62 | links: 63 | feedback: "Invia un riscontro" 64 | forgot_password: "Dimenticata la password?" 65 | remembered_password: "Fa' niente. Ricordo la mia password." 66 | notices: 67 | abandoned: "%{thing} dimenticato!" 68 | adopted: "Hai appena scelto un %{thing}!" 69 | password_reset: "Inviate le istruzioni per reimpostare la password! Controlla la tua casella postale elettronica." 70 | reminder_sent: "Promemoria inviato!" 71 | signed_in: "Entrato!" 72 | signed_out: "Uscito." 73 | signed_up: "Grazie per esserti iscritto!" 74 | stolen: "%{thing} rubato!" 75 | titles: 76 | adopt: "Scegli questo %{thing}" 77 | adopted: "%{thing_name} è stato scelto" 78 | byline: "da %{name}" 79 | edit_profile: "Modifica il tuo profilo" 80 | main: "Scegli-un-%{thing}" 81 | ofline: "di %{organization}" 82 | sign_in: "Accedi per scegliere questo %{thing}" 83 | thank_you: "Grazie per aver scelto questo %{thing}!" 84 | tos: "Condizioni contrattuali" 85 | sponsors: 86 | built: "Costruito in Boston" 87 | cfa: "Code for America" 88 | city: "Città di Boston" 89 | mayor: 90 | name: "Martin J. Walsh" 91 | title: "Sindaco" 92 | -------------------------------------------------------------------------------- /config/locales/es.yml: -------------------------------------------------------------------------------- 1 | # Please maintain alphabetical order 2 | 3 | es: 4 | buttons: 5 | abandon: "Abandona esta %{thing}" 6 | adopt: "Adoptar!" 7 | back: "De nuevo" 8 | change_password: "Cambiar mi contraseña" 9 | close: "Cerca" 10 | edit_profile: "Editar perfil" 11 | email_password: "Correo electrónico mi contraseña" 12 | find: "Encontrar %{thing}" 13 | send_reminder: "Enviar aviso a pala" 14 | sign_in: "Ingresar" 15 | sign_out: "Salir" 16 | sign_up: "Regístrate" 17 | update: "Actualización" 18 | captions: 19 | optional: "(opcional)" 20 | private: "(privado)" 21 | public: "(visible para los demás)" 22 | required: "(necesario)" 23 | defaults: 24 | address: "dirección" 25 | address_1: "1 City Hall Plaza" 26 | address_2: "Suite 500" 27 | city: "Boston" 28 | city_state: "Boston, Massachusetts" 29 | neighborhood: "Downtown" 30 | sms_number: "857-555-1212" 31 | state: "MA" 32 | thing: "boca de incendio" 33 | this_thing: "Esta %{thing}" 34 | tagline: "Reclamar la responsabilidad para palear un hidrante de incendios después de que las nieves." 35 | tos: "Al registrarse, usted está de acuerdo con los %{tos}." 36 | voice_number: "617-555-1212" 37 | zip: "02201-2013" 38 | errors: 39 | password: "Es necesario iniciar sesión o registrarse antes de continuar." 40 | not_found: "No se pudo encontrar %{thing}." 41 | labels: 42 | address: "Dirección, Barrio" 43 | address_1: "Dirección línea 1" 44 | address_2: "Dirección línea 2" 45 | city: "Ciudad" 46 | city_state: "Ciudad" 47 | email: "Dirección de correo electrónico" 48 | name: "Nombre" 49 | name_thing: "Dar a esta toma de agua un nombre" 50 | organization: "Organización" 51 | password: "Contraseña" 52 | password_choose: "Elija una contraseña" 53 | password_current: "Contraseña actual" 54 | password_new: "Una nueva contraseña" 55 | remember_me: "Mantener el" 56 | sms_number: "Número de teléfono móvil" 57 | state: "Estado" 58 | user_existing: "Ya he firmado" 59 | user_new: "No se ha inscrito" 60 | voice_number: "Número de teléfono" 61 | zip: "Código postal" 62 | links: 63 | feedback: "Envíenos sus comentarios" 64 | forgot_password: "¿Olvidaste tu contraseña?" 65 | remembered_password: "No importa. Me acordé de mi contraseña." 66 | notices: 67 | abandoned: "toma de agua abandonada!" 68 | adopted: "Usted se acaba de aprobar una %{thing}!" 69 | password_reset: "Las instrucciones de restablecimiento de contraseña enviado! Revise su correo electrónico." 70 | reminder_sent: "Recordatorio enviado!" 71 | signed_in: "Sesión!" 72 | signed_out: "La sesión." 73 | signed_up: "¡Gracias por registrarte!" 74 | stolen: "%{thing} robado!" 75 | titles: 76 | adopt: "Adoptar esta %{thing}" 77 | adopted: "%{thing_name} ha sido adoptada." 78 | byline: "por %{name}" 79 | edit_profile: "Edita tu perfil" 80 | main: "Adopt-a-%{thing}" 81 | ofline: "de %{organization}" 82 | sign_in: "Iniciar sesión para adoptar esta %{thing}" 83 | thank_you: "Gracias por la adopción de esta %{thing}!" 84 | tos: "Términos de Servicio" 85 | sponsors: 86 | built: "Construido en Boston" 87 | cfa: "Código de los Estados Unidos" 88 | city: "Ciudad de Boston" 89 | mayor: 90 | name: "Martin J. Walsh" 91 | title: "Mayor" 92 | -------------------------------------------------------------------------------- /config/locales/fr.yml: -------------------------------------------------------------------------------- 1 | # Please maintain alphabetical order 2 | 3 | fr: 4 | buttons: 5 | abandon: "Abandonner cette %{thing}" 6 | adopt: "Adopter!" 7 | back: "Retour" 8 | change_password: "Changer mon mot de passe" 9 | close: "Fermer" 10 | edit_profile: "Modifier le profil" 11 | email_password: "Envoyez-moi mon mot de passe" 12 | find: "Trouver %{thing}" 13 | send_reminder: "Envoyer un rappel à la pelle" 14 | sign_in: "Connexion" 15 | sign_out: "Inscription sur" 16 | sign_up: "S'inscrire" 17 | update: "Mise à jour" 18 | captions: 19 | optional: "(optionnelle)" 20 | private: "(privé)" 21 | public: "(visible pour les autres)" 22 | required: "(nécessaire)" 23 | defaults: 24 | address: "Adresse" 25 | address_1: "1 City Hall Plaza" 26 | address_2: "Suite 500" 27 | city: "Boston" 28 | city_state: "Boston, Massachusetts" 29 | neighborhood: "Downtown" 30 | sms_number: "857-555-1212" 31 | state: "MA" 32 | thing: "bouche d'incendie" 33 | this_thing: "Cette %{thing}" 34 | tagline: "La responsabilité Réclamation pour pelleter une %{thing} le feu après qu'il neige." 35 | tos: "En vous inscrivant, vous acceptez les %{tos}." 36 | voice_number: "617-555-1212" 37 | zip: "02201-2013" 38 | errors: 39 | password: "Vous devez vous identifier ou vous inscrire avant de continuer." 40 | not_found: "Impossible de trouver %{thing}." 41 | labels: 42 | address: "Adresse, Quartier" 43 | address_1: "Adresse ligne 1" 44 | address_2: "Adresse ligne 2" 45 | city: "Ville" 46 | city_state: "Ville" 47 | email: "Adresse e-mail" 48 | name: "Nom" 49 | name_thing: "Donnez cette %{thing} d'un nom" 50 | organization: "Organisation" 51 | password: "Mot de passe" 52 | password_choose: "Choisissez un mot de passe" 53 | password_current: "Mot de passe actuel" 54 | password_new: "Nouveau mot de passe" 55 | remember_me: "Rester connecté" 56 | sms_number: "Numéro de téléphone portable" 57 | state: "Etat" 58 | user_existing: "J'ai déjà signé" 59 | user_new: "Je n'ai pas encore inscrits" 60 | voice_number: "Le numéro de téléphone Accueil" 61 | zip: "Code postal" 62 | links: 63 | feedback: "Envoyer des commentaires" 64 | forgot_password: "Mot de passe oublié?" 65 | remembered_password: "Jamais l'esprit. J'ai rappelé mon mot de passe." 66 | notices: 67 | abandoned: "%{thing} abandonné!" 68 | adopted: "Vous avez juste a adopté une %{thing}!" 69 | password_reset: "Les instructions de réinitialisation de mot de passe envoyé! Vérifiez votre adresse email." 70 | reminder_sent: "Rappel envoyé!" 71 | signed_in: "Signé en!" 72 | signed_out: "Signé à." 73 | signed_up: "Merci pour votre inscription!" 74 | stolen: "%{thing} volé!" 75 | titles: 76 | adopt: "Adoptez cette %{thing}" 77 | adopted: "%{thing_name} a été adoptée" 78 | byline: "par %{name}" 79 | edit_profile: "Modifiez votre profil" 80 | main: "Adopt-a-%{thing}" 81 | ofline: "de %{organization}" 82 | sign_in: "Connectez-vous à adopter cette %{thing}" 83 | thank_you: "Merci pour l'adoption de cette %{thing}!" 84 | tos: "Conditions d'utilisation" 85 | sponsors: 86 | built: "Construit à Boston" 87 | cfa: "Code pour l'Amérique" 88 | city: "Ville de Boston" 89 | mayor: 90 | name: "Martin J. Walsh" 91 | title: "Mayor" 92 | -------------------------------------------------------------------------------- /config/locales/de.yml: -------------------------------------------------------------------------------- 1 | # Please maintain alphabetical order 2 | 3 | de: 4 | buttons: 5 | abandon: "Diesen %{thing} aufgeben" 6 | adopt: "Adoptieren" 7 | back: "Zurück" 8 | change_password: "Mein Passwort ändern" 9 | close: "Schließen" 10 | edit_profile: "Profil bearbeiten" 11 | email_password: "Bitte schicken Sie mir mein Passwort" 12 | find: "Finden Sie einen %{thing}" 13 | send_reminder: "Send Erinnerung zu schaufeln" 14 | sign_in: "Einloggen" 15 | sign_out: "Ausloggen" 16 | sign_up: "Registrieren" 17 | update: "Aktualisierung" 18 | captions: 19 | optional: "(optional)" 20 | private: "(privat)" 21 | public: "(für Andere sichtbar)" 22 | required: "(erforderlich)" 23 | defaults: 24 | address: "Adresse" 25 | address_1: "1 City Hall Plaza" 26 | address_2: "Suite 500" 27 | city: "Boston" 28 | city_state: "Boston, Massachusetts" 29 | neighborhood: "Downtown" 30 | sms_number: "857-555-1212" 31 | state: "MA" 32 | thing: "Hydrant" 33 | this_thing: "Dieser %{thing}" 34 | tagline: "Verantwortung dafür übernehmen, einen Hydranten auszubuddeln, nachdem es geschneit hat." 35 | tos: "Mit der Anmeldung erklären Sie sich mit den %{tos} einverstanden." 36 | voice_number: "617-555-1212" 37 | zip: "02201-2013" 38 | errors: 39 | password: "Sie müssen sich einloggen oder registrieren, um fortzufahren." 40 | not_found: "%{thing} konnte nicht gefunden werden." 41 | labels: 42 | address: "Adresse, Stadtviertel" 43 | address_1: "Adresszeile 1" 44 | address_2: "Adresszeile 2" 45 | city: "Stadt" 46 | city_state: "Stadt" 47 | email: "E-Mail-Adresse" 48 | name: "Name" 49 | name_thing: "Geben Sie diesem %{thing} einen Namen" 50 | organization: "Organisation" 51 | password: "Passwort" 52 | password_choose: "Wählen Sie ein Passwort" 53 | password_current: "Aktuelles Passwort" 54 | password_new: "Neues Passwort" 55 | remember_me: "Eingeloggt bleiben" 56 | sms_number: "Handy-Nummer" 57 | state: "Zustand" 58 | user_existing: "Ich habe mich bereits registriert" 59 | user_new: "Ich habe mich noch nicht registriert" 60 | voice_number: "Startseite Telefonnummer" 61 | zip: "Postleitzahl" 62 | links: 63 | feedback: "Feedback senden" 64 | forgot_password: "Passwort vergessen?" 65 | remembered_password: "Das macht nichts. Ich erinnerte mich an mein Passwort vergessen." 66 | notices: 67 | abandoned: "%{thing} verlassen!" 68 | adopted: "Sie haben soeben einen %{thing} adoptiert!" 69 | password_reset: "Password Reset Anleitung geschickt! Überprüfen Sie Ihre E-Mail." 70 | reminder_sent: "Reminder versendet!" 71 | signed_in: "Unterzeichnet in!" 72 | signed_out: "Abgemeldet." 73 | signed_up: "Vielen Dank für Ihre Anmeldung!" 74 | stolen: "%{thing} gestohlen!" 75 | titles: 76 | adopt: "Adoptieren diesen %{thing}" 77 | adopted: "{thing_name} wurde adoptiert" 78 | byline: "von %{name}" 79 | edit_profile: "Ihr Profil bearbeiten" 80 | main: "%{thing} adoptieren" 81 | ofline: "von %{organization}" 82 | sign_in: "Melden Sie sich an um diesen %{thing} zu übernehmen" 83 | thank_you: "Vielen Dank für die Annahme dieser %{thing}!" 84 | tos: "Nutzungsbedingungen" 85 | sponsors: 86 | built: "Erbaut in Boston" 87 | cfa: "Code for Amerika" 88 | city: "City of Boston" 89 | mayor: 90 | name: "Martin J. Walsh" 91 | title: "Mayor" 92 | -------------------------------------------------------------------------------- /app/views/sidebar/_combo_form.html.haml: -------------------------------------------------------------------------------- 1 | = form_for :user, :html => {:id => "combo-form", :class => "form-vertical"} do |f| 2 | %fieldset#common_fields 3 | .control-group 4 | %label{:for => "user_email", :id => "user_email_label"} 5 | = t("labels.email") 6 | %small 7 | = image_tag "lock.png", :class => "lock", :alt => t("captions.private"), :title => t("captions.private") 8 | = f.email_field "email", :value => params[:user] ? params[:user][:email] : nil 9 | .control-group.radio 10 | = f.label "new" , radio_button_tag("user", "new", true).html_safe + t("labels.user_new") 11 | = f.label "existing", radio_button_tag("user", "existing").html_safe + t("labels.user_existing") 12 | %fieldset#user_sign_up_fields 13 | .control-group 14 | %label{:for => "user_name", :id => "user_name_label"} 15 | = t("labels.name") 16 | %small 17 | = t("captions.public") 18 | = f.text_field "name" 19 | .control-group 20 | %label{:for => "user_organization", :id => "user_organization_label"} 21 | = t("labels.organization") 22 | %small 23 | = t("captions.public") 24 | = f.text_field "organization" 25 | .control-group 26 | %label{:for => "user_voice_number", :id => "user_voice_number_label"} 27 | = t("labels.voice_number") 28 | %small 29 | = image_tag "lock.png", :class => "lock", :alt => t("captions.private"), :title => t("captions.private") 30 | = f.telephone_field "voice_number", :placeholder => t("defaults.voice_number") 31 | .control-group 32 | %label{:for => "user_sms_number", :id => "user_sms_number_label"} 33 | = t("labels.sms_number") 34 | %small 35 | = image_tag "lock.png", :class => "lock", :alt => t("captions.private"), :title => t("captions.private") 36 | = f.telephone_field "sms_number", :placeholder => t("defaults.sms_number") 37 | .control-group 38 | %label{:for => "user_password_confirmation", :id => "user_password_confirmation_label"} 39 | = t("labels.password_choose") 40 | %small 41 | = image_tag "lock.png", :class => "lock", :alt => t("captions.private"), :title => t("captions.private") 42 | = f.password_field "password_confirmation" 43 | .form-actions 44 | = f.submit t("buttons.sign_up"), :class => "btn btn-primary" 45 | %p 46 | = t("defaults.tos", :tos => link_to(t("titles.tos"), "#tos", :id => "tos_link", :"data-toggle" => "modal")).html_safe 47 | %fieldset#user_sign_in_fields{:style => "display: none;"} 48 | .control-group 49 | %label{:for => "user_password", :id => "user_password_label"} 50 | = t("labels.password") 51 | %small 52 | = image_tag "lock.png", :class => "lock", :alt => t("captions.private"), :title => t("captions.private") 53 | = f.password_field "password" 54 | .control-group 55 | = f.label "remember_me" , f.check_box("remember_me", :checked => true).html_safe + t("labels.remember_me") 56 | .form-actions 57 | = f.submit t("buttons.sign_in"), :class => "btn btn-primary" 58 | %p 59 | = link_to t("links.forgot_password"), "#", :id => "user_forgot_password_link" 60 | %fieldset#user_forgot_password_fields{:style => "display: none;"} 61 | .form-actions 62 | = f.submit t("buttons.email_password"), :class => "btn btn-primary" 63 | %p 64 | = link_to t("links.remembered_password"), "#", :id => "user_remembered_password_link" 65 | = render :partial => "sidebar/tos" 66 | :javascript 67 | $(function() { 68 | $('#user_email').focus(); 69 | }); 70 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended that you check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(version: 5) do 15 | 16 | # These are extensions that must be enabled in order to support this database 17 | enable_extension "plpgsql" 18 | 19 | create_table "rails_admin_histories", force: :cascade do |t| 20 | t.string "message" 21 | t.string "username" 22 | t.integer "item" 23 | t.string "table" 24 | t.integer "month", limit: 2 25 | t.integer "year", limit: 8 26 | t.datetime "created_at" 27 | t.datetime "updated_at" 28 | end 29 | 30 | add_index "rails_admin_histories", ["item", "table", "month", "year"], name: "index_rails_admin_histories", using: :btree 31 | 32 | create_table "reminders", force: :cascade do |t| 33 | t.datetime "created_at" 34 | t.datetime "updated_at" 35 | t.integer "from_user_id", null: false 36 | t.integer "to_user_id", null: false 37 | t.integer "thing_id", null: false 38 | t.boolean "sent", default: false 39 | end 40 | 41 | add_index "reminders", ["from_user_id"], name: "index_reminders_on_from_user_id", using: :btree 42 | add_index "reminders", ["sent"], name: "index_reminders_on_sent", using: :btree 43 | add_index "reminders", ["thing_id"], name: "index_reminders_on_thing_id", using: :btree 44 | add_index "reminders", ["to_user_id"], name: "index_reminders_on_to_user_id", using: :btree 45 | 46 | create_table "things", force: :cascade do |t| 47 | t.datetime "created_at" 48 | t.datetime "updated_at" 49 | t.string "name" 50 | t.decimal "lat", precision: 16, scale: 14, null: false 51 | t.decimal "lng", precision: 17, scale: 14, null: false 52 | t.integer "city_id" 53 | t.integer "user_id" 54 | end 55 | 56 | add_index "things", ["city_id"], name: "index_things_on_city_id", unique: true, using: :btree 57 | 58 | create_table "users", force: :cascade do |t| 59 | t.datetime "created_at" 60 | t.datetime "updated_at" 61 | t.string "name", null: false 62 | t.string "organization" 63 | t.string "email", null: false 64 | t.string "voice_number" 65 | t.string "sms_number" 66 | t.string "address_1" 67 | t.string "address_2" 68 | t.string "city" 69 | t.string "state" 70 | t.string "zip" 71 | t.boolean "admin", default: false 72 | t.string "encrypted_password", default: "", null: false 73 | t.string "reset_password_token" 74 | t.datetime "reset_password_sent_at" 75 | t.datetime "remember_created_at" 76 | t.integer "sign_in_count", default: 0, null: false 77 | t.datetime "current_sign_in_at" 78 | t.datetime "last_sign_in_at" 79 | t.string "current_sign_in_ip" 80 | t.string "last_sign_in_ip" 81 | end 82 | 83 | add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree 84 | add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree 85 | 86 | end 87 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like 20 | # NGINX, varnish or squid. 21 | # config.action_dispatch.rack_cache = true 22 | 23 | # Disable serving static files from the `/public` folder by default since 24 | # Apache or NGINX already handles this. 25 | config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present? 26 | 27 | # Compress JavaScripts and CSS. 28 | config.assets.js_compressor = :uglifier 29 | # config.assets.css_compressor = :sass 30 | 31 | # Do not fallback to assets pipeline if a precompiled asset is missed. 32 | config.assets.compile = false 33 | 34 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 35 | # yet still be able to expire them through the digest params. 36 | config.assets.digest = true 37 | 38 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 39 | 40 | # Specifies the header that your server uses for sending files. 41 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 42 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 43 | 44 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 45 | # config.force_ssl = true 46 | 47 | # Use the lowest log level to ensure availability of diagnostic information 48 | # when problems arise. 49 | config.log_level = :debug 50 | 51 | # Prepend all log lines with the following tags. 52 | # config.log_tags = [ :subdomain, :uuid ] 53 | 54 | # Use a different logger for distributed setups. 55 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 56 | 57 | # Use a different cache store in production. 58 | # config.cache_store = :mem_cache_store 59 | 60 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 61 | # config.action_controller.asset_host = 'http://assets.example.com' 62 | 63 | # Ignore bad email addresses and do not raise email delivery errors. 64 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 65 | config.action_mailer.raise_delivery_errors = true 66 | config.action_mailer.delivery_method = :smtp 67 | config.action_mailer.default_url_options = {host: 'adoptahydrant.org'} 68 | 69 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 70 | # the I18n.default_locale when a translation cannot be found). 71 | config.i18n.fallbacks = true 72 | 73 | # Send deprecation notices to registered listeners. 74 | config.active_support.deprecation = :notify 75 | 76 | # Use default logging formatter so that PID and timestamp are not suppressed. 77 | config.log_formatter = ::Logger::Formatter.new 78 | 79 | # Do not dump schema after migrations. 80 | config.active_record.dump_schema_after_migration = false 81 | end 82 | 83 | ActionMailer::Base.smtp_settings = { 84 | address: 'smtp.sendgrid.net', 85 | port: '25', 86 | authentication: :plain, 87 | user_name: ENV['SENDGRID_USERNAME'], 88 | password: ENV['SENDGRID_PASSWORD'], 89 | domain: ENV['SENDGRID_DOMAIN'], 90 | } 91 | -------------------------------------------------------------------------------- /config/locales/devise.en.yml: -------------------------------------------------------------------------------- 1 | # Additional translations at https://github.com/plataformatec/devise/wiki/I18n 2 | 3 | en: 4 | devise: 5 | confirmations: 6 | confirmed: "Your account was successfully confirmed." 7 | send_instructions: "You will receive an email with instructions about how to confirm your account in a few minutes." 8 | send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions about how to confirm your account in a few minutes." 9 | failure: 10 | already_authenticated: "You are already signed in." 11 | inactive: "Your account is not activated yet." 12 | invalid: "Invalid email or password." 13 | locked: "Your account is locked." 14 | last_attempt: "You have one more attempt before your account will be locked." 15 | not_found_in_database: "Invalid email or password." 16 | timeout: "Your session expired. Please sign in again to continue." 17 | unauthenticated: "You need to sign in or sign up before continuing." 18 | unconfirmed: "You have to confirm your account before continuing." 19 | mailer: 20 | confirmation_instructions: 21 | subject: "Confirmation instructions" 22 | reset_password_instructions: 23 | subject: "Reset password instructions" 24 | unlock_instructions: 25 | subject: "Unlock Instructions" 26 | omniauth_callbacks: 27 | failure: "Could not authenticate you from %{kind} because \"%{reason}\"." 28 | success: "Successfully authenticated from %{kind} account." 29 | passwords: 30 | no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." 31 | send_instructions: "You will receive an email with instructions about how to reset your password in a few minutes." 32 | send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." 33 | updated: "Your password was changed successfully. You are now signed in." 34 | updated_not_active: "Your password was changed successfully." 35 | registrations: 36 | destroyed: "Bye! Your account was successfully cancelled. We hope to see you again soon." 37 | signed_up: "Welcome! You have signed up successfully." 38 | signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." 39 | signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." 40 | signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please open the link to activate your account." 41 | update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and click on the confirm link to finalize confirming your new email address." 42 | updated: "You updated your account successfully." 43 | sessions: 44 | signed_in: "Signed in successfully." 45 | signed_out: "Signed out successfully." 46 | unlocks: 47 | send_instructions: "You will receive an email with instructions about how to unlock your account in a few minutes." 48 | send_paranoid_instructions: "If your account exists, you will receive an email with instructions about how to unlock it in a few minutes." 49 | unlocked: "Your account has been unlocked successfully. Please sign in to continue." 50 | errors: 51 | messages: 52 | already_confirmed: "was already confirmed, please try signing in" 53 | confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" 54 | expired: "has expired, please request a new one" 55 | not_found: "not found" 56 | not_locked: "was not locked" 57 | not_saved: 58 | one: "1 error prohibited this %{resource} from being saved:" 59 | other: "%{count} errors prohibited this %{resource} from being saved:" 60 | -------------------------------------------------------------------------------- /app/views/sidebar/edit_profile.html.haml: -------------------------------------------------------------------------------- 1 | = form_for resource, :as => resource_name, :url => registration_path(resource_name), :html => {:id => "edit_form", :method => :put} do |f| 2 | = f.hidden_field "id" 3 | %fieldset.control-group 4 | %label{:for => "user_email", :id => "user_email_label"} 5 | = t("labels.email") 6 | %small 7 | = image_tag "lock.png", :class => "lock", :alt => t("captions.private"), :title => t("captions.private") 8 | = f.email_field "email" 9 | %fieldset.control-group 10 | %label{:for => "user_name", :id => "user_name_label"} 11 | = t("labels.name") 12 | %small 13 | = t("captions.public") 14 | = f.text_field "name" 15 | %fieldset.control-group 16 | %label{:for => "user_organization", :id => "user_organization_label"} 17 | = t("labels.organization") 18 | %small 19 | = t("captions.public") 20 | = f.text_field "organization" 21 | %fieldset.control-group 22 | %label{:for => "user_voice_number", :id => "user_voice_number_label"} 23 | = t("labels.voice_number") 24 | %small 25 | = image_tag "lock.png", :class => "lock", :alt => t("captions.private"), :title => t("captions.private") 26 | = f.telephone_field "voice_number", :placeholder => t("defaults.voice_number"), :value => number_to_phone(f.object.voice_number) 27 | %fieldset.control-group 28 | %label{:for => "user_sms_number", :id => "user_sms_number_label"} 29 | = t("labels.sms_number") 30 | %small 31 | = image_tag "lock.png", :class => "lock", :alt => t("captions.private"), :title => t("captions.private") 32 | = f.telephone_field "sms_number", :placeholder => t("defaults.sms_number"), :value => number_to_phone(f.object.sms_number) 33 | %fieldset.control-group 34 | %label{:for => "user_address_1", :id => "user_address_1_label"} 35 | = t("labels.address_1") 36 | %small 37 | = image_tag "lock.png", :class => "lock", :alt => t("captions.private"), :title => t("captions.private") 38 | = f.text_field "address_1", :placeholder => t("defaults.address_1") 39 | %fieldset.control-group 40 | %label{:for => "user_address_2", :id => "user_address_2_label"} 41 | = t("labels.address_2") 42 | %small 43 | = image_tag "lock.png", :class => "lock", :alt => t("captions.private"), :title => t("captions.private") 44 | = f.text_field "address_2", :placeholder => t("defaults.address_2") 45 | %fieldset.control-group 46 | %label{:for => "user_city", :id => "user_city_label"} 47 | = t("labels.city") 48 | %small 49 | = image_tag "lock.png", :class => "lock", :alt => t("captions.private"), :title => t("captions.private") 50 | = f.text_field "city", :placeholder => t("defaults.city") 51 | %fieldset.control-group 52 | %label{:for => "user_state", :id => "user_state_label"} 53 | = t("labels.state") 54 | %small 55 | = image_tag "lock.png", :class => "lock", :alt => t("captions.private"), :title => t("captions.private") 56 | = f.select "state", us_states, :include_blank => true 57 | %fieldset.control-group 58 | %label{:for => "user_zip", :id => "user_zip_label"} 59 | = t("labels.zip") 60 | %small 61 | = image_tag "lock.png", :class => "lock", :alt => t("captions.private"), :title => t("captions.private") 62 | = f.text_field "zip", :placeholder => t("defaults.zip") 63 | %fieldset.control-group 64 | %label{:for => "user_password", :id => "user_password_label"} 65 | = t("labels.password_new") 66 | %small 67 | = t("captions.optional") 68 | = f.password_field "password" 69 | %fieldset.control-group 70 | %label{:for => "user_current_password", :id => "user_current_password_label"} 71 | = t("labels.password_current") 72 | %small 73 | = t("captions.required") 74 | = f.password_field "current_password" 75 | %fieldset.form-actions 76 | = f.submit t("buttons.update"), :class => "btn btn-primary" 77 | %a{:href => root_path, :id => "back_link", :class => "btn"} 78 | = t("buttons.back") 79 | :javascript 80 | $(function() { 81 | $('#user_email').focus(); 82 | }); 83 | -------------------------------------------------------------------------------- /app/assets/stylesheets/screen.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | height: 100%; 7 | margin: 0; 8 | } 9 | 10 | h1, h2 { 11 | text-align: center; 12 | margin: 20px 0 10px; 13 | } 14 | 15 | h2 { 16 | font-size: 22px; 17 | line-height: 30px; 18 | margin: 10px 0; 19 | } 20 | 21 | form { 22 | margin-bottom: 0; 23 | } 24 | 25 | label { 26 | text-align: left; 27 | } 28 | 29 | input { 30 | width: 200px; 31 | } 32 | 33 | .sidebar input, .sidebar select, .sidebar option, .sidebar button, .sidebar form.search-form input, .sidebar form.search-form select, .sidebar form.search-form option, .sidebar a.btn { 34 | display: block; 35 | margin-bottom: 10px; 36 | } 37 | 38 | input[type="radio"], input[type="checkbox"] { 39 | display: inline; 40 | width: auto; 41 | margin-right: 10px; 42 | } 43 | 44 | input#user_new { 45 | margin-bottom: 0; 46 | } 47 | 48 | input#user_existing { 49 | margin-bottom: 10px; 50 | } 51 | 52 | form select, button.btn, input[type="submit"].btn { 53 | width: 210px; 54 | } 55 | 56 | form input.search-query { 57 | -moz-border-radius: 14px; 58 | -webkit-border-radius: 14px; 59 | border-radius: 14px; 60 | margin-bottom: 0; 61 | padding-left: 14px; 62 | padding-right: 14px; 63 | width: 180px; 64 | } 65 | 66 | .sidebar p { 67 | padding: 0 20px; 68 | margin: 0; 69 | } 70 | 71 | a.btn { 72 | width: 188px; 73 | } 74 | 75 | .table { 76 | display: table; 77 | height: 100%; 78 | width: 100%; 79 | } 80 | 81 | .table-row { 82 | display: table-row; 83 | } 84 | 85 | .table-cell { 86 | display: table-cell; 87 | height: 100%; 88 | vertical-align: top; 89 | } 90 | 91 | .alert-message.block-message { 92 | padding: 8px 12px 8px; 93 | margin-bottom: 10px; 94 | } 95 | 96 | .sidebar .alert-message.block-message { 97 | -webkit-border-radius: 0; 98 | -moz-border-radius: 0; 99 | border-radius: 0; 100 | text-align: left; 101 | padding-left: 10px; 102 | } 103 | 104 | .sidebar { 105 | width: 250px; 106 | border-right: 1px solid #ccc; 107 | text-align: center; 108 | } 109 | 110 | .sidebar .control-group { 111 | margin-left: 20px; 112 | } 113 | 114 | .sidebar .form-actions { 115 | padding: 17px 20px 10px; 116 | } 117 | 118 | .sidebar p#tagline { 119 | color: #ffffff; 120 | background-color: #c43c35; 121 | background-repeat: repeat-x; 122 | background-image: -khtml-gradient(linear, left top, left bottom, from(#ee5f5b), to(#c43c35)); 123 | background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35); 124 | background-image: -ms-linear-gradient(top, #ee5f5b, #c43c35); 125 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ee5f5b), color-stop(100%, #c43c35)); 126 | background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35); 127 | background-image: -o-linear-gradient(top, #ee5f5b, #c43c35); 128 | background-image: linear-gradient(top, #ee5f5b, #c43c35); 129 | border-color: #c43c35 #c43c35 #882a25; 130 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 131 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0); 132 | padding: 10px 20px; 133 | text-align: center; 134 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 135 | } 136 | 137 | .sidebar #logos { 138 | height: 100px; 139 | } 140 | 141 | .sidebar #logos #col1 { 142 | float: left; 143 | margin-left: 20px; 144 | width: 100px; 145 | } 146 | 147 | .sidebar #logos #col1 img { 148 | float: left; 149 | margin: 5px 0; 150 | } 151 | 152 | .sidebar #logos #col2 { 153 | float: left; 154 | margin-left: 10px; 155 | width: 100px; 156 | } 157 | 158 | .sidebar #mayor { 159 | font-family: "Times New Roman", Times, serif; 160 | font-size: 8px; 161 | padding: 0; 162 | margin: -5px 0 0 0; 163 | } 164 | 165 | .sidebar #mayor a, .sidebar #mayor a:active, .sidebar #mayor a:hover, .sidebar #mayor a:visited { 166 | color: #000; 167 | text-decoration: none; 168 | } 169 | 170 | .sidebar #feedback { 171 | margin-top: 5px; 172 | } 173 | 174 | .map-container { 175 | width: auto; 176 | } 177 | 178 | #map { 179 | height: 100%; 180 | width: 100%; 181 | } 182 | 183 | #tos { 184 | text-align: left; 185 | } 186 | 187 | #tos p { 188 | padding: 0; 189 | margin-bottom: 10px; 190 | } 191 | 192 | .upcase { 193 | text-transform: uppercase; 194 | } 195 | 196 | .alpha { 197 | list-style-type: lower-alpha; 198 | } 199 | 200 | .roman { 201 | list-style-type: lower-roman; 202 | } 203 | 204 | img.lock { 205 | height: 9px; 206 | width: 7px; 207 | opacity: 0.8; 208 | filter: alpha(opacity=80); /* For IE8 and earlier */ 209 | } 210 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Adopt-a-Hydrant 2 | 3 | [![Build Status](http://img.shields.io/travis/codeforamerica/adopt-a-hydrant.svg)][travis] 4 | [![Dependency Status](http://img.shields.io/gemnasium/codeforamerica/adopt-a-hydrant.svg)][gemnasium] 5 | [![Coverage Status](http://img.shields.io/coveralls/codeforamerica/adopt-a-hydrant.svg)][coveralls] 6 | 7 | [travis]: http://travis-ci.org/codeforamerica/adopt-a-hydrant 8 | [gemnasium]: https://gemnasium.com/codeforamerica/adopt-a-hydrant 9 | [coveralls]: https://coveralls.io/r/codeforamerica/adopt-a-hydrant 10 | 11 | Claim responsibility for shoveling out a fire hydrant after it snows. 12 | 13 | ## Screenshot 14 | ![Adopt-a-Hydrant](https://github.com/codeforamerica/adopt-a-hydrant/raw/master/screenshot.png "Adopt-a-Hydrant") 15 | 16 | ## Demo 17 | You can see a running version of the application at 18 | [http://adopt-a-hydrant.herokuapp.com/][demo]. 19 | 20 | [demo]: http://adopt-a-hydrant.herokuapp.com/ 21 | 22 | ## Installation 23 | This application requires [Postgres](http://www.postgresql.org/) to be installed 24 | 25 | git clone git://github.com/codeforamerica/adopt-a-hydrant.git 26 | cd adopt-a-hydrant 27 | bundle install 28 | 29 | bundle exec rake db:create 30 | bundle exec rake db:schema:load 31 | 32 | ## Usage 33 | rails server 34 | 35 | ## Seed Data 36 | bundle exec rake db:seed 37 | 38 | ## Deploying to Heroku 39 | A successful deployment to Heroku requires a few setup steps: 40 | 41 | 1. Generate a new secret token: 42 | 43 | ``` 44 | rake secret 45 | ``` 46 | 47 | 2. Set the token on Heroku: 48 | 49 | ``` 50 | heroku config:set SECRET_TOKEN=the_token_you_generated 51 | ``` 52 | 53 | 3. [Precompile your assets](https://devcenter.heroku.com/articles/rails3x-asset-pipeline-cedar) 54 | 55 | ``` 56 | RAILS_ENV=production bundle exec rake assets:precompile 57 | 58 | git add public/assets 59 | 60 | git commit -m "vendor compiled assets" 61 | ``` 62 | 63 | 4. Add a production database to config/database.yml 64 | 65 | 5. Seed the production db: 66 | 67 | `heroku run bundle exec rake db:seed` 68 | 69 | Keep in mind that the Heroku free Postgres plan only allows up to 10,000 rows, 70 | so if your city has more than 10,000 fire hydrants (or other thing to be 71 | adopted), you will need to upgrade to the $9/month plan. 72 | 73 | ### Google Analytics 74 | If you have a Google Analytics account you want to use to track visits to your 75 | deployment of this app, just set your ID and your domain name as environment 76 | variables: 77 | 78 | heroku config:set GOOGLE_ANALYTICS_ID=your_id 79 | heroku config:set GOOGLE_ANALYTICS_DOMAIN=your_domain_name 80 | 81 | An example ID is `UA-12345678-9`, and an example domain is `adoptahydrant.org`. 82 | 83 | ## Contributing 84 | In the spirit of [free software][free-sw], **everyone** is encouraged to help 85 | improve this project. 86 | 87 | [free-sw]: http://www.fsf.org/licensing/essays/free-sw.html 88 | 89 | Here are some ways *you* can contribute: 90 | 91 | * by using alpha, beta, and prerelease versions 92 | * by reporting bugs 93 | * by suggesting new features 94 | * by [translating to a new language][locales] 95 | * by writing or editing documentation 96 | * by writing specifications 97 | * by writing code (**no patch is too small**: fix typos, add comments, clean up 98 | inconsistent whitespace) 99 | * by refactoring code 100 | * by closing [issues][] 101 | * by reviewing patches 102 | * [financially][] 103 | 104 | [locales]: https://github.com/codeforamerica/adopt-a-hydrant/tree/master/config/locales 105 | [issues]: https://github.com/codeforamerica/adopt-a-hydrant/issues 106 | [financially]: https://secure.codeforamerica.org/page/contribute 107 | 108 | ## Submitting an Issue 109 | We use the [GitHub issue tracker][issues] to track bugs and features. Before 110 | submitting a bug report or feature request, check to make sure it hasn't 111 | already been submitted. When submitting a bug report, please include a [Gist][] 112 | that includes a stack trace and any details that may be necessary to reproduce 113 | the bug, including your gem version, Ruby version, and operating system. 114 | Ideally, a bug report should include a pull request with failing specs. 115 | 116 | [gist]: https://gist.github.com/ 117 | 118 | ## Submitting a Pull Request 119 | 1. [Fork the repository.][fork] 120 | 2. [Create a topic branch.][branch] 121 | 3. Add specs for your unimplemented feature or bug fix. 122 | 4. Run `bundle exec rake test`. If your specs pass, return to step 3. 123 | 5. Implement your feature or bug fix. 124 | 6. Run `bundle exec rake test`. If your specs fail, return to step 5. 125 | 7. Run `open coverage/index.html`. If your changes are not completely covered 126 | by your tests, return to step 3. 127 | 8. Add, commit, and push your changes. 128 | 9. [Submit a pull request.][pr] 129 | 130 | [fork]: http://help.github.com/fork-a-repo/ 131 | [branch]: http://learn.github.com/p/branching.html 132 | [pr]: http://help.github.com/send-pull-requests/ 133 | 134 | ## Supported Ruby Version 135 | This library aims to support and is [tested against][travis] Ruby version 2.3.0. 136 | 137 | If something doesn't work on this version, it should be considered a bug. 138 | 139 | This library may inadvertently work (or seem to work) on other Ruby 140 | implementations, however support will only be provided for the version above. 141 | 142 | If you would like this library to support another Ruby version, you may 143 | volunteer to be a maintainer. Being a maintainer entails making sure all tests 144 | run and pass on that implementation. When something breaks on your 145 | implementation, you will be personally responsible for providing patches in a 146 | timely fashion. If critical issues for a particular implementation exist at the 147 | time of a major release, support for that Ruby version may be dropped. 148 | 149 | ## Copyright 150 | Copyright (c) 2014 Code for America. See [LICENSE][] for details. 151 | 152 | [license]: https://github.com/codeforamerica/adopt-a-hydrant/blob/master/LICENSE.md 153 | 154 | [![Code for America Tracker](http://stats.codeforamerica.org/codeforamerica/adopt-a-hydrant.png)][tracker] 155 | 156 | [tracker]: http://stats.codeforamerica.org/projects/adopt-a-hydrant 157 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actionmailer (4.2.7.1) 5 | actionpack (= 4.2.7.1) 6 | actionview (= 4.2.7.1) 7 | activejob (= 4.2.7.1) 8 | mail (~> 2.5, >= 2.5.4) 9 | rails-dom-testing (~> 1.0, >= 1.0.5) 10 | actionpack (4.2.7.1) 11 | actionview (= 4.2.7.1) 12 | activesupport (= 4.2.7.1) 13 | rack (~> 1.6) 14 | rack-test (~> 0.6.2) 15 | rails-dom-testing (~> 1.0, >= 1.0.5) 16 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 17 | actionview (4.2.7.1) 18 | activesupport (= 4.2.7.1) 19 | builder (~> 3.1) 20 | erubis (~> 2.7.0) 21 | rails-dom-testing (~> 1.0, >= 1.0.5) 22 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 23 | activejob (4.2.7.1) 24 | activesupport (= 4.2.7.1) 25 | globalid (>= 0.3.0) 26 | activemodel (4.2.7.1) 27 | activesupport (= 4.2.7.1) 28 | builder (~> 3.1) 29 | activerecord (4.2.7.1) 30 | activemodel (= 4.2.7.1) 31 | activesupport (= 4.2.7.1) 32 | arel (~> 6.0) 33 | activesupport (4.2.7.1) 34 | i18n (~> 0.7) 35 | json (~> 1.7, >= 1.7.7) 36 | minitest (~> 5.1) 37 | thread_safe (~> 0.3, >= 0.3.4) 38 | tzinfo (~> 1.1) 39 | addressable (2.4.0) 40 | arel (6.0.3) 41 | ast (2.3.0) 42 | bcrypt (3.1.11) 43 | builder (3.2.2) 44 | coffee-rails (4.2.1) 45 | coffee-script (>= 2.2.0) 46 | railties (>= 4.0.0, < 5.2.x) 47 | coffee-script (2.4.1) 48 | coffee-script-source 49 | execjs 50 | coffee-script-source (1.10.0) 51 | concurrent-ruby (1.0.2) 52 | coveralls (0.8.15) 53 | json (>= 1.8, < 3) 54 | simplecov (~> 0.12.0) 55 | term-ansicolor (~> 1.3) 56 | thor (~> 0.19.1) 57 | tins (>= 1.6.0, < 2) 58 | crack (0.4.3) 59 | safe_yaml (~> 1.0.0) 60 | devise (4.2.0) 61 | bcrypt (~> 3.0) 62 | orm_adapter (~> 0.1) 63 | railties (>= 4.1.0, < 5.1) 64 | responders 65 | warden (~> 1.2.3) 66 | docile (1.1.5) 67 | erubis (2.7.0) 68 | execjs (2.7.0) 69 | fastercsv (1.5.5) 70 | font-awesome-rails (4.6.3.1) 71 | railties (>= 3.2, < 5.1) 72 | geokit (1.10.0) 73 | globalid (0.3.7) 74 | activesupport (>= 4.1.0) 75 | haml (4.0.7) 76 | tilt 77 | hashdiff (0.3.0) 78 | http_accept_language (2.0.5) 79 | i18n (0.7.0) 80 | jquery-rails (4.2.1) 81 | rails-dom-testing (>= 1, < 3) 82 | railties (>= 4.2.0) 83 | thor (>= 0.14, < 2.0) 84 | jquery-ui-rails (5.0.5) 85 | railties (>= 3.2.16) 86 | json (1.8.3) 87 | kaminari (0.17.0) 88 | actionpack (>= 3.0.0) 89 | activesupport (>= 3.0.0) 90 | loofah (2.0.3) 91 | nokogiri (>= 1.5.9) 92 | mail (2.6.4) 93 | mime-types (>= 1.16, < 4) 94 | mime-types (3.1) 95 | mime-types-data (~> 3.2015) 96 | mime-types-data (3.2016.0521) 97 | mini_portile2 (2.1.0) 98 | minitest (5.9.1) 99 | nested_form (0.3.2) 100 | nokogiri (1.6.8.1) 101 | mini_portile2 (~> 2.1.0) 102 | orm_adapter (0.5.0) 103 | parser (2.3.1.4) 104 | ast (~> 2.2) 105 | pg (0.19.0) 106 | powerpack (0.1.1) 107 | puma (3.6.0) 108 | rack (1.6.4) 109 | rack-pjax (1.0.0) 110 | nokogiri (~> 1.5) 111 | rack (>= 1.1) 112 | rack-test (0.6.3) 113 | rack (>= 1.0) 114 | rails (4.2.7.1) 115 | actionmailer (= 4.2.7.1) 116 | actionpack (= 4.2.7.1) 117 | actionview (= 4.2.7.1) 118 | activejob (= 4.2.7.1) 119 | activemodel (= 4.2.7.1) 120 | activerecord (= 4.2.7.1) 121 | activesupport (= 4.2.7.1) 122 | bundler (>= 1.3.0, < 2.0) 123 | railties (= 4.2.7.1) 124 | sprockets-rails 125 | rails-deprecated_sanitizer (1.0.3) 126 | activesupport (>= 4.2.0.alpha) 127 | rails-dom-testing (1.0.7) 128 | activesupport (>= 4.2.0.beta, < 5.0) 129 | nokogiri (~> 1.6.0) 130 | rails-deprecated_sanitizer (>= 1.0.1) 131 | rails-html-sanitizer (1.0.3) 132 | loofah (~> 2.0) 133 | rails_12factor (0.0.3) 134 | rails_serve_static_assets 135 | rails_stdout_logging 136 | rails_admin (1.0.0) 137 | builder (~> 3.1) 138 | coffee-rails (~> 4.0) 139 | font-awesome-rails (>= 3.0, < 5) 140 | haml (~> 4.0) 141 | jquery-rails (>= 3.0, < 5) 142 | jquery-ui-rails (~> 5.0) 143 | kaminari (~> 0.14) 144 | nested_form (~> 0.3) 145 | rack-pjax (>= 0.7) 146 | rails (>= 4.0, < 6) 147 | remotipart (~> 1.3) 148 | sass-rails (>= 4.0, < 6) 149 | rails_serve_static_assets (0.0.5) 150 | rails_stdout_logging (0.0.5) 151 | railties (4.2.7.1) 152 | actionpack (= 4.2.7.1) 153 | activesupport (= 4.2.7.1) 154 | rake (>= 0.8.7) 155 | thor (>= 0.18.1, < 2.0) 156 | rainbow (2.1.0) 157 | rake (11.3.0) 158 | remotipart (1.3.1) 159 | responders (2.3.0) 160 | railties (>= 4.2.0, < 5.1) 161 | rubocop (0.43.0) 162 | parser (>= 2.3.1.1, < 3.0) 163 | powerpack (~> 0.1) 164 | rainbow (>= 1.99.1, < 3.0) 165 | ruby-progressbar (~> 1.7) 166 | unicode-display_width (~> 1.0, >= 1.0.1) 167 | ruby-progressbar (1.8.1) 168 | safe_yaml (1.0.4) 169 | sass (3.4.22) 170 | sass-rails (5.0.6) 171 | railties (>= 4.0.0, < 6) 172 | sass (~> 3.1) 173 | sprockets (>= 2.8, < 4.0) 174 | sprockets-rails (>= 2.0, < 4.0) 175 | tilt (>= 1.1, < 3) 176 | simplecov (0.12.0) 177 | docile (~> 1.1.0) 178 | json (>= 1.8, < 3) 179 | simplecov-html (~> 0.10.0) 180 | simplecov-html (0.10.0) 181 | skylight (0.10.6) 182 | activesupport (>= 3.0.0) 183 | spring (2.0.0) 184 | activesupport (>= 4.2) 185 | sprockets (3.7.0) 186 | concurrent-ruby (~> 1.0) 187 | rack (> 1, < 3) 188 | sprockets-rails (3.2.0) 189 | actionpack (>= 4.0) 190 | activesupport (>= 4.0) 191 | sprockets (>= 3.0.0) 192 | sqlite3 (1.3.12) 193 | term-ansicolor (1.4.0) 194 | tins (~> 1.0) 195 | thor (0.19.1) 196 | thread_safe (0.3.5) 197 | tilt (2.0.5) 198 | tins (1.12.0) 199 | tzinfo (1.2.2) 200 | thread_safe (~> 0.1) 201 | uglifier (3.0.2) 202 | execjs (>= 0.3.0, < 3) 203 | unicode-display_width (1.1.1) 204 | validates_formatting_of (0.9.0) 205 | activemodel 206 | warden (1.2.6) 207 | rack (>= 1.0) 208 | webmock (2.1.0) 209 | addressable (>= 2.3.6) 210 | crack (>= 0.3.2) 211 | hashdiff 212 | 213 | PLATFORMS 214 | ruby 215 | 216 | DEPENDENCIES 217 | arel 218 | coveralls 219 | devise 220 | fastercsv 221 | geokit 222 | haml 223 | http_accept_language 224 | nokogiri 225 | pg 226 | puma 227 | rails (~> 4.2.7) 228 | rails_12factor 229 | rails_admin 230 | rubocop 231 | sass-rails (>= 4.0.3) 232 | simplecov 233 | skylight 234 | spring 235 | sqlite3 236 | uglifier 237 | validates_formatting_of 238 | webmock 239 | 240 | RUBY VERSION 241 | ruby 2.3.1p112 242 | 243 | BUNDLED WITH 244 | 1.13.2 245 | -------------------------------------------------------------------------------- /config/initializers/devise.rb: -------------------------------------------------------------------------------- 1 | # Use this hook to configure devise mailer, warden hooks and so forth. 2 | # Many of these configuration options can be set straight in your model. 3 | Devise.setup do |config| 4 | # The secret key used by Devise. Devise uses this key to generate 5 | # random tokens. Changing this key will render invalid all existing 6 | # confirmation, reset password and unlock tokens in the database. 7 | config.secret_key = 'e642a15001ccf8126bf88426c41497c97d28084fd1de3b2f136ef897257e3e6de525fb751199e439cbefc9c93bc643c59426852c44f4c108dfcb87fdf7cfd503' 8 | 9 | # ==> Mailer Configuration 10 | # Configure the e-mail address which will be shown in Devise::Mailer, 11 | # note that it will be overwritten if you use your own mailer class 12 | # with default "from" parameter. 13 | config.mailer_sender = 'noreply@adoptahydrant.com' 14 | 15 | # Configure the class responsible to send e-mails. 16 | # config.mailer = 'Devise::Mailer' 17 | 18 | # ==> ORM configuration 19 | # Load and configure the ORM. Supports :active_record (default) and 20 | # :mongoid (bson_ext recommended) by default. Other ORMs may be 21 | # available as additional gems. 22 | require 'devise/orm/active_record' 23 | 24 | # ==> Configuration for any authentication mechanism 25 | # Configure which keys are used when authenticating a user. The default is 26 | # just :email. You can configure it to use [:username, :subdomain], so for 27 | # authenticating a user, both parameters are required. Remember that those 28 | # parameters are used only when authenticating and not when retrieving from 29 | # session. If you need permissions, you should implement that in a before filter. 30 | # You can also supply a hash where the value is a boolean determining whether 31 | # or not authentication should be aborted when the value is not present. 32 | # config.authentication_keys = [ :email ] 33 | 34 | # Configure parameters from the request object used for authentication. Each entry 35 | # given should be a request method and it will automatically be passed to the 36 | # find_for_authentication method and considered in your model lookup. For instance, 37 | # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. 38 | # The same considerations mentioned for authentication_keys also apply to request_keys. 39 | # config.request_keys = [] 40 | 41 | # Configure which authentication keys should be case-insensitive. 42 | # These keys will be downcased upon creating or modifying a user and when used 43 | # to authenticate or find a user. Default is :email. 44 | config.case_insensitive_keys = [:email] 45 | 46 | # Configure which authentication keys should have whitespace stripped. 47 | # These keys will have whitespace before and after removed upon creating or 48 | # modifying a user and when used to authenticate or find a user. Default is :email. 49 | config.strip_whitespace_keys = [:email] 50 | 51 | # Tell if authentication through request.params is enabled. True by default. 52 | # It can be set to an array that will enable params authentication only for the 53 | # given strategies, for example, `config.params_authenticatable = [:database]` will 54 | # enable it only for database (email + password) authentication. 55 | # config.params_authenticatable = true 56 | 57 | # Tell if authentication through HTTP Auth is enabled. False by default. 58 | # It can be set to an array that will enable http authentication only for the 59 | # given strategies, for example, `config.http_authenticatable = [:database]` will 60 | # enable it only for database authentication. The supported strategies are: 61 | # :database = Support basic authentication with authentication key + password 62 | # config.http_authenticatable = false 63 | 64 | # If http headers should be returned for AJAX requests. True by default. 65 | # config.http_authenticatable_on_xhr = true 66 | 67 | # The realm used in Http Basic Authentication. 'Application' by default. 68 | # config.http_authentication_realm = 'Application' 69 | 70 | # It will change confirmation, password recovery and other workflows 71 | # to behave the same regardless if the e-mail provided was right or wrong. 72 | # Does not affect registerable. 73 | # config.paranoid = true 74 | 75 | # By default Devise will store the user in session. You can skip storage for 76 | # particular strategies by setting this option. 77 | # Notice that if you are skipping storage for all authentication paths, you 78 | # may want to disable generating routes to Devise's sessions controller by 79 | # passing :skip => :sessions to `devise_for` in your config/routes.rb 80 | config.skip_session_storage = [:http_auth] 81 | 82 | # By default, Devise cleans up the CSRF token on authentication to 83 | # avoid CSRF token fixation attacks. This means that, when using AJAX 84 | # requests for sign in and sign up, you need to get a new CSRF token 85 | # from the server. You can disable this option at your own risk. 86 | # config.clean_up_csrf_token_on_authentication = true 87 | 88 | # ==> Configuration for :database_authenticatable 89 | # For bcrypt, this is the cost for hashing the password and defaults to 10. If 90 | # using other encryptors, it sets how many times you want the password re-encrypted. 91 | # 92 | # Limiting the stretches to just one in testing will increase the performance of 93 | # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use 94 | # a value less than 10 in other environments. 95 | config.stretches = Rails.env.test? ? 1 : 10 96 | 97 | # Setup a pepper to generate the encrypted password. 98 | config.pepper = 'd0ce05a602094357144e8d2ce90258904f8cb26fb943cefd6fe0b824752616a9254fadabed3a47ba5c0de66a359513768ab1ab233d9cfef893f376a9b5ebcf68' 99 | 100 | # ==> Configuration for :confirmable 101 | # A period that the user is allowed to access the website even without 102 | # confirming his account. For instance, if set to 2.days, the user will be 103 | # able to access the website for two days without confirming his account, 104 | # access will be blocked just in the third day. Default is 0.days, meaning 105 | # the user cannot access the website without confirming his account. 106 | # config.allow_unconfirmed_access_for = 2.days 107 | 108 | # A period that the user is allowed to confirm their account before their 109 | # token becomes invalid. For example, if set to 3.days, the user can confirm 110 | # their account within 3 days after the mail was sent, but on the fourth day 111 | # their account can't be confirmed with the token any more. 112 | # Default is nil, meaning there is no restriction on how long a user can take 113 | # before confirming their account. 114 | # config.confirm_within = 3.days 115 | 116 | # If true, requires any email changes to be confirmed (exactly the same way as 117 | # initial account confirmation) to be applied. Requires additional unconfirmed_email 118 | # db field (see migrations). Until confirmed new email is stored in 119 | # unconfirmed email column, and copied to email column on successful confirmation. 120 | config.reconfirmable = true 121 | 122 | # Defines which key will be used when confirming an account 123 | # config.confirmation_keys = [ :email ] 124 | 125 | # ==> Configuration for :rememberable 126 | # The time the user will be remembered without asking for credentials again. 127 | config.remember_for = 1.year 128 | 129 | # If true, extends the user's remember period when remembered via cookie. 130 | config.extend_remember_period = true 131 | 132 | # Options to be passed to the created cookie. For instance, you can set 133 | # :secure => true in order to force SSL only cookies. 134 | # config.rememberable_options = {} 135 | 136 | # ==> Configuration for :validatable 137 | # Range for password length. Default is 8..128. 138 | # config.password_length = 8..128 139 | 140 | # Email regex used to validate email formats. It simply asserts that 141 | # one (and only one) @ exists in the given string. This is mainly 142 | # to give user feedback and not to assert the e-mail validity. 143 | # config.email_regexp = /\A[^@]+@[^@]+\z/ 144 | 145 | # ==> Configuration for :timeoutable 146 | # The time you want to timeout the user session without activity. After this 147 | # time the user will be asked for credentials again. Default is 30 minutes. 148 | # config.timeout_in = 30.minutes 149 | 150 | # If true, expires auth token on session timeout. 151 | # config.expire_auth_token_on_timeout = false 152 | 153 | # ==> Configuration for :lockable 154 | # Defines which strategy will be used to lock an account. 155 | # :failed_attempts = Locks an account after a number of failed attempts to sign in. 156 | # :none = No lock strategy. You should handle locking by yourself. 157 | # config.lock_strategy = :failed_attempts 158 | 159 | # Defines which key will be used when locking and unlocking an account 160 | # config.unlock_keys = [ :email ] 161 | 162 | # Defines which strategy will be used to unlock an account. 163 | # :email = Sends an unlock link to the user email 164 | # :time = Re-enables login after a certain amount of time (see :unlock_in below) 165 | # :both = Enables both strategies 166 | # :none = No unlock strategy. You should handle unlocking by yourself. 167 | # config.unlock_strategy = :both 168 | 169 | # Number of authentication tries before locking an account if lock_strategy 170 | # is failed attempts. 171 | # config.maximum_attempts = 20 172 | 173 | # Time interval to unlock the account if :time is enabled as unlock_strategy. 174 | # config.unlock_in = 1.hour 175 | 176 | # Warn on the last attempt before the account is locked. 177 | # config.last_attempt_warning = false 178 | 179 | # ==> Configuration for :recoverable 180 | # 181 | # Defines which key will be used when recovering the password for an account 182 | # config.reset_password_keys = [ :email ] 183 | 184 | # Time interval you can reset your password with a reset password key. 185 | # Don't put a too small interval or your users won't have the time to 186 | # change their passwords. 187 | config.reset_password_within = 6.hours 188 | 189 | # ==> Configuration for :encryptable 190 | # Allow you to use another encryption algorithm besides bcrypt (default). You can use 191 | # :sha1, :sha512 or encryptors from others authentication tools as :clearance_sha1, 192 | # :authlogic_sha512 (then you should set stretches above to 20 for default behavior) 193 | # and :restful_authentication_sha1 (then you should set stretches to 10, and copy 194 | # REST_AUTH_SITE_KEY to pepper). 195 | # 196 | # Require the `devise-encryptable` gem when using anything other than bcrypt 197 | # config.encryptor = :sha512 198 | 199 | # ==> Scopes configuration 200 | # Turn scoped views on. Before rendering "sessions/new", it will first check for 201 | # "users/sessions/new". It's turned off by default because it's slower if you 202 | # are using only default views. 203 | # config.scoped_views = false 204 | 205 | # Configure the default scope given to Warden. By default it's the first 206 | # devise role declared in your routes (usually :user). 207 | # config.default_scope = :user 208 | 209 | # Set this configuration to false if you want /users/sign_out to sign out 210 | # only the current scope. By default, Devise signs out all scopes. 211 | # config.sign_out_all_scopes = true 212 | 213 | # ==> Navigation configuration 214 | # Lists the formats that should be treated as navigational. Formats like 215 | # :html, should redirect to the sign in page when the user does not have 216 | # access, but formats like :xml or :json, should return 401. 217 | # 218 | # If you have any extra navigational formats, like :iphone or :mobile, you 219 | # should add them to the navigational formats lists. 220 | # 221 | # The "*/*" below is required to match Internet Explorer requests. 222 | # config.navigational_formats = ['*/*', :html] 223 | 224 | # The default HTTP method used to sign out a resource. Default is :delete. 225 | config.sign_out_via = :delete 226 | 227 | # ==> OmniAuth 228 | # Add a new OmniAuth provider. Check the wiki for more information on setting 229 | # up on your models and hooks. 230 | # config.omniauth :github, 'APP_ID', 'APP_SECRET', :scope => 'user,public_repo' 231 | 232 | # ==> Warden configuration 233 | # If you want to use other strategies, that are not supported by Devise, or 234 | # change the failure app, you can configure them inside the config.warden block. 235 | # 236 | # config.warden do |manager| 237 | # manager.intercept_401 = false 238 | # manager.default_strategies(:scope => :user).unshift :some_external_strategy 239 | # end 240 | 241 | # ==> Mountable engine configurations 242 | # When using Devise inside an engine, let's call it `MyEngine`, and this engine 243 | # is mountable, there are some extra configurations to be taken into account. 244 | # The following options are available, assuming the engine is mounted as: 245 | # 246 | # mount MyEngine, at: '/my_engine' 247 | # 248 | # The router that invoked `devise_for`, in the example above, would be: 249 | # config.router_name = :my_engine 250 | # 251 | # When using omniauth, Devise cannot automatically set Omniauth path, 252 | # so you need to do it manually. For the users scope, it would be: 253 | # config.omniauth_path_prefix = '/my_engine/users/auth' 254 | end 255 | -------------------------------------------------------------------------------- /app/views/sidebar/_tos.html.haml: -------------------------------------------------------------------------------- 1 | .modal.hide.fade#tos{:style => "display: none;"} 2 | .modal-header 3 | %a{:class => "close", :"data-dismiss" => "modal", :href => "#"} 4 | × 5 | %h2 6 | = t("titles.tos") 7 | .modal-body 8 | %ol 9 | %li 10 | %h3 11 | Acceptance of Terms 12 | %p 13 | The City of Boston ("City") provides the Adopt-a-Hydrant program ("AAH") to you subject to the following Terms of Service Agreement ("Agreement") which may be updated by us from time to time without notice to you. By accessing and using AAH, you accept and agree to be bound by the terms and provision of this Agreement. 14 | %li 15 | %h3 16 | Description of Adopt-a-Hydrant 17 | %p 18 | Adopt-a-Hydrant is an initiative of the City to encourage residents to shovel out snowed-in fire hydrants. The AAH website allows users to "adopt" a fire hydrant, or state that you intend to help shovel that fire hydrant out if it gets covered by snow. 19 | %p 20 | Unless explicitly stated otherwise, any new features that augment or enhance AAH are covered by this Agreement. 21 | %li 22 | %h3 23 | Obligations 24 | %p 25 | If you choose to adopt a fire hydrant using the AAH website, you are under no obligation and have no responsibility to actually shovel out that fire hydrant or any fire hydrant. 26 | %li 27 | %h3 28 | Termination of Obligations 29 | %p 30 | If you provide any information that is untrue, inaccurate, offensive, not current, or incomplete, or the City has reasonable grounds to suspect that such information is untrue, inaccurate, offensive, not current, or incomplete, the City has the right to suspend or terminate your account and refuse any and all current or future use of the AAH services (or any portion thereof). 31 | %p 32 | Accounts that consistently adopt fire hydrants without shoveling them out may also be terminated. 33 | %p 34 | Additionally, you agree that the City may, without prior notice, immediately terminate, limit your access to, or suspend your AAH service. Cause for such termination, limitation of access, or suspension shall include but not be limited to: 35 | %ol.alpha 36 | %li 37 | breaches or violations of this Agreement or other incorporated agreements or guidelines, 38 | %li 39 | requests by law enforcement or other government agencies, 40 | %li 41 | discontinuance or material modification to AAH (or any part thereof), 42 | %li 43 | unexpected technical or security issues or problems, 44 | %li 45 | extended periods of inactivity, 46 | %li 47 | engagement by you in fraudulent or illegal activities. 48 | %p 49 | Further, you agree that all terminations, limitations of access, and suspensions for cause shall be made in the City's sole discretion and that the City shall not be liable to you or any third party for any termination of your account, any associated email address, or access to AAH. 50 | %li 51 | %h3 52 | City Public Records Policy 53 | %p 54 | Registration data, as well as data you submit using AAH, are subject to all applicable City, State and Federal public records laws. 55 | %li 56 | %h3 57 | Member Conduct 58 | %p 59 | You understand that all information entered by the user is the sole responsibility of the person from whom such Content originated. This means that you, and not the City, are entirely responsible for all Content that you submit or otherwise make available via AAH. The City does not control the sign-in content posted and, as such, does not guarantee the accuracy, integrity, or quality of such Content. You understand that by using AAH, you may be exposed to Content that is offensive, indecent, or objectionable. Under no circumstances will the City be liable in any way for any Content, including, but not limited to, any errors or omissions in any Content, or any loss or damage of any kind incurred as a result of the use of any Content submitted or otherwise made available via AAH. 60 | %p 61 | We are under no obligation to enforce this Agreement on your behalf against another user. While we encourage you to let us know if you believe another user violated this Agreement, we reserve the right to investigate and take appropriate action at the City's sole discretion. 62 | %p 63 | You agree to not use AAH to: 64 | %ol.alpha 65 | %li 66 | submit Content that is false or inaccurate; 67 | %li 68 | submit Content that does not generally pertain to the designated topic or theme; 69 | %li 70 | submit or otherwise make available any Content that is unlawful, harmful, threatening, abusive, harassing, tortuous, defamatory, vulgar, obscene, libelous, invasive of another's privacy, hateful, or racially, ethnically, or otherwise objectionable; 71 | %li 72 | harm minors in any way; 73 | %li 74 | impersonate any person or entity, or falsely state or otherwise misrepresent your affiliation with a person or entity; 75 | %li 76 | submit or otherwise make available any Content that you do not have a right to make available under any law or under contractual or fiduciary relationships (such as inside information, proprietary and confidential information learned or disclosed as part of employment relationships or under nondisclosure agreements); 77 | %li 78 | submit or otherwise make available any Content that infringes any patent, trademark, trade secret, copyright, or other proprietary rights ("Rights") of any party; 79 | %li 80 | submit or otherwise make available any unsolicited or unauthorized advertising, promotional materials, "junk mail," "spam," "chain letters," "pyramid schemes," or any other form of communication not relevant to specific City service requests; 81 | %li 82 | submit or otherwise make available any material that contains software viruses or any other computer code, files or programs designed to interrupt, destroy or limit the functionality of any computer software or hardware or telecommunications equipment; 83 | %li 84 | intentionally or unintentionally violate any applicable local, state, national or international law; 85 | %li 86 | "stalk" or otherwise harass another; or 87 | %li 88 | collect or store personal data about other users in connection with the prohibited conduct and activities set forth in paragraphs (a) through (k) above. 89 | %p 90 | You acknowledge that the City may or may not pre-screen Content that is shared, but that the City and its designees shall have the right (but not the obligation) in their sole discretion to pre-screen, refuse, or remove any Content that is available via AAH. Without limiting the foregoing, the City and its designees shall have the right to remove any Content that violates this Agreement or is otherwise objectionable. You agree that you must evaluate, and bear all risks associated with, the use of any Content, including any reliance on the accuracy, completeness, or usefulness of such Content. 91 | %p 92 | You acknowledge, consent and agree that the City may access, preserve and disclose your account information and Content if required to do so by law or in a good faith belief that such access preservation or disclosure is reasonably necessary to: 93 | %ol.roman 94 | %li 95 | comply with legal process; 96 | %li 97 | enforce this Agreement; 98 | %li 99 | respond to claims that any Content violates the rights of third parties; 100 | %li 101 | respond to your requests for customer service; 102 | %li 103 | protect the rights, property or personal safety of the City, its users and the public. 104 | %p 105 | You may not attempt to override or circumvent any of the usage rules embedded into AAH. 106 | %p 107 | You may not trace any information of any other AAH user or visitor or otherwise use AAH for the purpose of obtaining information of any other AAH user. 108 | %p 109 | You may not attempt to gain unauthorized access to AAH or the computer systems and networks connected to AAH through hacking, password mining, or any other means. 110 | %p 111 | You may not release the results of any performance or functional evaluation of AAH to any third party without prior written approval of the City for each such release. 112 | %p 113 | You may not take any action that imposes, or may impose, in the City's sole discretion, an unreasonable or disproportionately large load on the City's technology infrastructure or otherwise make excessive traffic demands of AAH. 114 | %li 115 | %h3 116 | Interstate Nature of Communications on Adopt-a-Hydrant 117 | %p 118 | When you use the application, you acknowledge that in using AAH, you will be causing communications to be sent potentially through a variety of networks (Internet service provider, wireless phone network, etc.) As a result, even communications that seem to be intrastate in nature can result in the transmission of interstate communications regardless of where you are physically located at the time of transmission. Accordingly, you agree that use of the AAH may result in interstate data transmissions. 119 | %li 120 | %h3 121 | Submissions to the City 122 | %p 123 | By submitting feedback ("Submissions") to the City through the AAH feedback function, you acknowledge and agree that: 124 | %ol.alpha 125 | %li 126 | your Submissions do not contain confidential or proprietary information; 127 | %li 128 | the City is not under any obligation of confidentiality, express or implied, with respect to the Submissions; 129 | %li 130 | the City shall be entitled to use or disclose (or choose not to use or disclose) such Submissions for any purpose, in any way, in any media worldwide; 131 | %li 132 | the City may have something similar to the Submissions already under consideration or in development; 133 | %li 134 | your Submissions automatically become the property of the City without any obligation of the City to you; 135 | %li 136 | you are not entitled to any compensation or reimbursement of any kind from the City under any circumstances. 137 | %li 138 | %h3 139 | Indemnity 140 | %p 141 | You agree to indemnify and hold the City and its subsidiaries, affiliates, officers, agents, employees, partners, and licensors harmless from any claim or demand (including reasonable attorneys' fees) made by any third party due to or arising out of: 142 | %ol.alpha 143 | %li 144 | Content you submit or otherwise make available through AAH, 145 | %li 146 | your use of AAH, 147 | %li 148 | your connection to AAH, 149 | %li 150 | your violation of this Agreement, 151 | %li 152 | your violation of any Rights of another. 153 | %li 154 | %h3 155 | Modifications to Adopt-a-Hydrant 156 | %p 157 | The City reserves the right at any time and from time to time to modify or discontinue, temporarily or permanently, AAH (or any part thereof) with or without notice. You agree that the City shall not be liable to you or to any third party for any modification, suspension or discontinuance of the AAH (or any part thereof). 158 | %li 159 | %h3 160 | Links 161 | %p 162 | The City may provide links to other World Wide Web sites or resources. You acknowledge and agree that the City is not responsible for the availability of such external sites or resources. 163 | %p 164 | AAH may facilitate your use of third party services not provided by the City ("Third Party Services"). The City makes no representations or warranties regarding the performance of such Third Party Services, their compliance with applicable laws and regulations, or any other aspects of such Third Party Services. Your use of Third Party Services is at your own risk and you are solely responsible for complying with all legal and contractual requirements necessary for using Third Party Services. 165 | %li 166 | %h3 167 | The City's Proprietary Rights 168 | %p 169 | You acknowledge and agree that AAH contains proprietary and confidential information that is owned by the City and protected by applicable intellectual property and other laws. You will not use such proprietary information in any way whatsoever except for use of AAH in compliance with the provisions of this Agreement. 170 | %h4.upcase 171 | Disclaimer of Warranties 172 | %p.upcase 173 | You expressly understand and agree that: 174 | %ol.alpha.upcase 175 | %li 176 | Your use of AAH is at your sole risk. The application is provided on an "as is" and "as available" basis. The city expressly disclaims all warranties of any kind, whether express or implied, including, but not limited to the implied warranties of title, merchantability, fitness for a particular purpose and non-infringement. 177 | %li 178 | The city and its subsidiaries, affiliates, officers, employees, agents, partners, and licensors make no warrany that: 179 | %ol.roman 180 | %li 181 | AAH will meet your requirements; 182 | %li 183 | AAH will be uninterrupted, timely, secore, or error-free; 184 | %li 185 | the results that may be obtained from the use of AAH will be accurate or reliable; 186 | %li 187 | any errors in the software will be corrected; 188 | %li 189 | AAH will be free from corruption, viruses, hacking, or other security intrusion. 190 | %li 191 | %h3 192 | Limitation of Liability 193 | %p.upcase 194 | You expressly understand and agree that the city shall not be liable to you for any punitive, indirect, incidental, special, consequential, or exemplary damages, including, but not limtied to, damages for loss of profits, goodwill, use, data, or other intangible losses resulting from: 195 | %ol.alpha.upcase 196 | %li 197 | Your access to or use of or inability to access or use the services; 198 | %li 199 | any conduct or content or any third party on the services, including without limitation, any defamatory, offensive or illegal conduct of other users or third parties; 200 | %li 201 | any content obtained from the services; 202 | %li 203 | unauthorized access, use or alteration of your transmissions or content, whether based on warranty, contract, tort (including negligence) or any other legal theory, whether or not the city has been informed of the possibility of such damage, and even if a remedy set forth herein is found to have failed of its essential purpose. 204 | %p.upcase 205 | You expressly agree that the city is not responsible for any contact or interaction between you and any other user of AAH and that you bear the sole risk of transmitting through the application any content, including information which identifies you or your location. 206 | %p.upcase 207 | You agree that the negation of damages set forth above is a fundamental element of the basis of the bargain between the city and you and that the foregoing limitation of liability is an agreed upon allocation of risk between you and the city. You acknowledge that absent your agreement to this limitation of liability the city would not provide AAH to you. 208 | %li 209 | %h3 210 | Notice 211 | %p 212 | The City may provide you with notices regarding AAH, including changes to this Agreement, by email, SMS, text message, postings on AAH, or any other reasonable means now known or hereafter developed. Notices shall be effective immediately. 213 | %p 214 | Such notices may not be received if you violate this Agreement by accessing the AAH in an unauthorized manner. You agree that you will be deemed to have received any and all notices that would have been delivered had you accessed AAH in an authorized manner. 215 | %li 216 | %h3 217 | Minor 218 | %p 219 | You represent that you are 13 years of age or older and, if you are under the age of 18, you either are an emancipated minor, or have obtained the legal consent of your parent or legal guardian to accept and agree to be bound by the terms and provisions of this Agreement. 220 | %p 221 | If you have agreed to allow your minor child, or a child for whom you are legal guardian ("Minor"), to use AAH, you agree that you shall be solely responsible for: 222 | %ol.alpha 223 | %li 224 | the online conduct of such Minor, 225 | %li 226 | monitoring such Minor’s access to and use of AAH, and 227 | %li 228 | the consequences of any use of AAH by such Minor. 229 | %li 230 | %h3 231 | Governing Law and Venue 232 | %p 233 | This Agreement is governed by the laws of the Commonwealth of Massachusetts without regard to any conflict of law provisions. You agree that any claim or dispute with the City relating in any way to your use of AAH shall be brought exclusively before a state or federal court sitting in Boston, Massachusetts, and you irrevocably waive any jurisdictional, venue, or inconvenient forum objections to such courts. 234 | %li 235 | %h3 236 | Miscellaneous 237 | %p 238 | This Agreement constitutes the entire agreement between you and the City with respect to your use of AAH, superseding any prior version of this Agreement between you and the City with respect to AAH. 239 | %p 240 | If any provision of this Agreement shall be unlawful, void, or unenforceable, then that provision shall be deemed severed from this Agreement and shall not affect the validity or enforceability of the remaining provisions of this Agreement. 241 | %p 242 | The City's failure to assert any right or provision under this Agreement shall not constitute a waiver of such right or provision. 243 | %p 244 | You agree that any cause of action arising out of or related to your use of AAH must commence within one year after the cause of action accrues. Otherwise, such cause of action is permanently barred. 245 | %p 246 | The section titles in this Agreement are for convenience only and have no legal or contractual effect. 247 | %p 248 | Last updated August 10, 2011. 249 | -------------------------------------------------------------------------------- /test/fixtures/city_hall.json: -------------------------------------------------------------------------------- 1 | { 2 | "results" : [ 3 | { 4 | "address_components" : [ 5 | { 6 | "long_name" : "Boston City Hall", 7 | "short_name" : "Boston City Hall", 8 | "types" : [ "point_of_interest", "establishment" ] 9 | }, 10 | { 11 | "long_name" : "1", 12 | "short_name" : "1", 13 | "types" : [ "street_number" ] 14 | }, 15 | { 16 | "long_name" : "City Hall Square", 17 | "short_name" : "City Hall Square", 18 | "types" : [ "route" ] 19 | }, 20 | { 21 | "long_name" : "Downtown", 22 | "short_name" : "Downtown", 23 | "types" : [ "neighborhood", "political" ] 24 | }, 25 | { 26 | "long_name" : "Boston", 27 | "short_name" : "Boston", 28 | "types" : [ "locality", "political" ] 29 | }, 30 | { 31 | "long_name" : "Suffolk", 32 | "short_name" : "Suffolk", 33 | "types" : [ "administrative_area_level_2", "political" ] 34 | }, 35 | { 36 | "long_name" : "Massachusetts", 37 | "short_name" : "MA", 38 | "types" : [ "administrative_area_level_1", "political" ] 39 | }, 40 | { 41 | "long_name" : "United States", 42 | "short_name" : "US", 43 | "types" : [ "country", "political" ] 44 | }, 45 | { 46 | "long_name" : "02201", 47 | "short_name" : "02201", 48 | "types" : [ "postal_code" ] 49 | }, 50 | { 51 | "long_name" : "1001", 52 | "short_name" : "1001", 53 | "types" : [] 54 | } 55 | ], 56 | "formatted_address" : "Boston City Hall, 1 City Hall Square, Boston, MA 02201, USA", 57 | "geometry" : { 58 | "location" : { 59 | "lat" : 42.360406, 60 | "lng" : -71.057993 61 | }, 62 | "location_type" : "APPROXIMATE", 63 | "viewport" : { 64 | "northeast" : { 65 | "lat" : 42.3686503, 66 | "lng" : -71.0419856 67 | }, 68 | "southwest" : { 69 | "lat" : 42.3521606, 70 | "lng" : -71.0740004 71 | } 72 | } 73 | }, 74 | "partial_match" : true, 75 | "types" : [ 76 | "city_hall", 77 | "point_of_interest", 78 | "local_government_office", 79 | "locality", 80 | "political", 81 | "establishment" 82 | ] 83 | }, 84 | { 85 | "address_components" : [ 86 | { 87 | "long_name" : "Faneuil Hall", 88 | "short_name" : "Faneuil Hall", 89 | "types" : [ "point_of_interest", "establishment" ] 90 | }, 91 | { 92 | "long_name" : "Boston National Historical Park", 93 | "short_name" : "Boston National Historical Park", 94 | "types" : [ "establishment" ] 95 | }, 96 | { 97 | "long_name" : "1", 98 | "short_name" : "1", 99 | "types" : [ "street_number" ] 100 | }, 101 | { 102 | "long_name" : "Faneuil Hall Square", 103 | "short_name" : "Faneuil Hall Square", 104 | "types" : [ "route" ] 105 | }, 106 | { 107 | "long_name" : "Downtown", 108 | "short_name" : "Downtown", 109 | "types" : [ "neighborhood", "political" ] 110 | }, 111 | { 112 | "long_name" : "Boston", 113 | "short_name" : "Boston", 114 | "types" : [ "locality", "political" ] 115 | }, 116 | { 117 | "long_name" : "Boston", 118 | "short_name" : "Boston", 119 | "types" : [ "administrative_area_level_3", "political" ] 120 | }, 121 | { 122 | "long_name" : "Suffolk", 123 | "short_name" : "Suffolk", 124 | "types" : [ "administrative_area_level_2", "political" ] 125 | }, 126 | { 127 | "long_name" : "Massachusetts", 128 | "short_name" : "MA", 129 | "types" : [ "administrative_area_level_1", "political" ] 130 | }, 131 | { 132 | "long_name" : "United States", 133 | "short_name" : "US", 134 | "types" : [ "country", "political" ] 135 | }, 136 | { 137 | "long_name" : "02109", 138 | "short_name" : "02109", 139 | "types" : [ "postal_code" ] 140 | } 141 | ], 142 | "formatted_address" : "Faneuil Hall, Boston National Historical Park, 1 Faneuil Hall Square, Boston, MA 02109, USA", 143 | "geometry" : { 144 | "location" : { 145 | "lat" : 42.3600619, 146 | "lng" : -71.05610299999999 147 | }, 148 | "location_type" : "APPROXIMATE", 149 | "viewport" : { 150 | "northeast" : { 151 | "lat" : 42.36141088029149, 152 | "lng" : -71.05475401970848 153 | }, 154 | "southwest" : { 155 | "lat" : 42.3587129197085, 156 | "lng" : -71.0574519802915 157 | } 158 | } 159 | }, 160 | "partial_match" : true, 161 | "types" : [ "point_of_interest", "establishment" ] 162 | }, 163 | { 164 | "address_components" : [ 165 | { 166 | "long_name" : "Hall Street", 167 | "short_name" : "Hall St", 168 | "types" : [ "route" ] 169 | }, 170 | { 171 | "long_name" : "Jamaica Plain", 172 | "short_name" : "Jamaica Plain", 173 | "types" : [ "sublocality", "political" ] 174 | }, 175 | { 176 | "long_name" : "Boston", 177 | "short_name" : "Boston", 178 | "types" : [ "locality", "political" ] 179 | }, 180 | { 181 | "long_name" : "Suffolk", 182 | "short_name" : "Suffolk", 183 | "types" : [ "administrative_area_level_2", "political" ] 184 | }, 185 | { 186 | "long_name" : "Massachusetts", 187 | "short_name" : "MA", 188 | "types" : [ "administrative_area_level_1", "political" ] 189 | }, 190 | { 191 | "long_name" : "United States", 192 | "short_name" : "US", 193 | "types" : [ "country", "political" ] 194 | }, 195 | { 196 | "long_name" : "02130", 197 | "short_name" : "02130", 198 | "types" : [ "postal_code" ] 199 | } 200 | ], 201 | "formatted_address" : "Hall Street, Boston, MA 02130, USA", 202 | "geometry" : { 203 | "bounds" : { 204 | "northeast" : { 205 | "lat" : 42.3054659, 206 | "lng" : -71.1118637 207 | }, 208 | "southwest" : { 209 | "lat" : 42.3046598, 210 | "lng" : -71.1146641 211 | } 212 | }, 213 | "location" : { 214 | "lat" : 42.3048254, 215 | "lng" : -71.1134469 216 | }, 217 | "location_type" : "GEOMETRIC_CENTER", 218 | "viewport" : { 219 | "northeast" : { 220 | "lat" : 42.3064118302915, 221 | "lng" : -71.1118637 222 | }, 223 | "southwest" : { 224 | "lat" : 42.3037138697085, 225 | "lng" : -71.1146641 226 | } 227 | } 228 | }, 229 | "partial_match" : true, 230 | "types" : [ "route" ] 231 | }, 232 | { 233 | "address_components" : [ 234 | { 235 | "long_name" : "City Hall Square", 236 | "short_name" : "City Hall Square", 237 | "types" : [ "route" ] 238 | }, 239 | { 240 | "long_name" : "Downtown", 241 | "short_name" : "Downtown", 242 | "types" : [ "neighborhood", "political" ] 243 | }, 244 | { 245 | "long_name" : "Boston", 246 | "short_name" : "Boston", 247 | "types" : [ "locality", "political" ] 248 | }, 249 | { 250 | "long_name" : "Suffolk", 251 | "short_name" : "Suffolk", 252 | "types" : [ "administrative_area_level_2", "political" ] 253 | }, 254 | { 255 | "long_name" : "Massachusetts", 256 | "short_name" : "MA", 257 | "types" : [ "administrative_area_level_1", "political" ] 258 | }, 259 | { 260 | "long_name" : "United States", 261 | "short_name" : "US", 262 | "types" : [ "country", "political" ] 263 | } 264 | ], 265 | "formatted_address" : "City Hall Square, Boston, MA, USA", 266 | "geometry" : { 267 | "bounds" : { 268 | "northeast" : { 269 | "lat" : 42.360398, 270 | "lng" : -71.05729599999999 271 | }, 272 | "southwest" : { 273 | "lat" : 42.3579646, 274 | "lng" : -71.0593976 275 | } 276 | }, 277 | "location" : { 278 | "lat" : 42.360248, 279 | "lng" : -71.0574544 280 | }, 281 | "location_type" : "GEOMETRIC_CENTER", 282 | "viewport" : { 283 | "northeast" : { 284 | "lat" : 42.3605302802915, 285 | "lng" : -71.05699781970849 286 | }, 287 | "southwest" : { 288 | "lat" : 42.3578323197085, 289 | "lng" : -71.05969578029149 290 | } 291 | } 292 | }, 293 | "partial_match" : true, 294 | "types" : [ "route" ] 295 | }, 296 | { 297 | "address_components" : [ 298 | { 299 | "long_name" : "Newhall Street", 300 | "short_name" : "Newhall St", 301 | "types" : [ "route" ] 302 | }, 303 | { 304 | "long_name" : "Dorchester", 305 | "short_name" : "Dorchester", 306 | "types" : [ "neighborhood", "political" ] 307 | }, 308 | { 309 | "long_name" : "Boston", 310 | "short_name" : "Boston", 311 | "types" : [ "locality", "political" ] 312 | }, 313 | { 314 | "long_name" : "Suffolk", 315 | "short_name" : "Suffolk", 316 | "types" : [ "administrative_area_level_2", "political" ] 317 | }, 318 | { 319 | "long_name" : "Massachusetts", 320 | "short_name" : "MA", 321 | "types" : [ "administrative_area_level_1", "political" ] 322 | }, 323 | { 324 | "long_name" : "United States", 325 | "short_name" : "US", 326 | "types" : [ "country", "political" ] 327 | }, 328 | { 329 | "long_name" : "02122", 330 | "short_name" : "02122", 331 | "types" : [ "postal_code" ] 332 | } 333 | ], 334 | "formatted_address" : "Newhall Street, Boston, MA 02122, USA", 335 | "geometry" : { 336 | "bounds" : { 337 | "northeast" : { 338 | "lat" : 42.2893203, 339 | "lng" : -71.050439 340 | }, 341 | "southwest" : { 342 | "lat" : 42.2877758, 343 | "lng" : -71.051587 344 | } 345 | }, 346 | "location" : { 347 | "lat" : 42.2886742, 348 | "lng" : -71.05117170000001 349 | }, 350 | "location_type" : "GEOMETRIC_CENTER", 351 | "viewport" : { 352 | "northeast" : { 353 | "lat" : 42.2898970302915, 354 | "lng" : -71.04966401970849 355 | }, 356 | "southwest" : { 357 | "lat" : 42.2871990697085, 358 | "lng" : -71.05236198029149 359 | } 360 | } 361 | }, 362 | "partial_match" : true, 363 | "types" : [ "route" ] 364 | }, 365 | { 366 | "address_components" : [ 367 | { 368 | "long_name" : "City Hall Avenue", 369 | "short_name" : "City Hall Avenue", 370 | "types" : [ "route" ] 371 | }, 372 | { 373 | "long_name" : "Downtown", 374 | "short_name" : "Downtown", 375 | "types" : [ "neighborhood", "political" ] 376 | }, 377 | { 378 | "long_name" : "Boston", 379 | "short_name" : "Boston", 380 | "types" : [ "locality", "political" ] 381 | }, 382 | { 383 | "long_name" : "Suffolk", 384 | "short_name" : "Suffolk", 385 | "types" : [ "administrative_area_level_2", "political" ] 386 | }, 387 | { 388 | "long_name" : "Massachusetts", 389 | "short_name" : "MA", 390 | "types" : [ "administrative_area_level_1", "political" ] 391 | }, 392 | { 393 | "long_name" : "United States", 394 | "short_name" : "US", 395 | "types" : [ "country", "political" ] 396 | } 397 | ], 398 | "formatted_address" : "City Hall Avenue, Boston, MA, USA", 399 | "geometry" : { 400 | "bounds" : { 401 | "northeast" : { 402 | "lat" : 42.3582279, 403 | "lng" : -71.058887 404 | }, 405 | "southwest" : { 406 | "lat" : 42.357693, 407 | "lng" : -71.0593676 408 | } 409 | }, 410 | "location" : { 411 | "lat" : 42.358086, 412 | "lng" : -71.05902329999999 413 | }, 414 | "location_type" : "GEOMETRIC_CENTER", 415 | "viewport" : { 416 | "northeast" : { 417 | "lat" : 42.3593094302915, 418 | "lng" : -71.05777831970849 419 | }, 420 | "southwest" : { 421 | "lat" : 42.3566114697085, 422 | "lng" : -71.0604762802915 423 | } 424 | } 425 | }, 426 | "partial_match" : true, 427 | "types" : [ "route" ] 428 | }, 429 | { 430 | "address_components" : [ 431 | { 432 | "long_name" : "Hall Place", 433 | "short_name" : "Hall Pl", 434 | "types" : [ "route" ] 435 | }, 436 | { 437 | "long_name" : "Telegraph Hill", 438 | "short_name" : "Telegraph Hill", 439 | "types" : [ "neighborhood", "political" ] 440 | }, 441 | { 442 | "long_name" : "Boston", 443 | "short_name" : "Boston", 444 | "types" : [ "locality", "political" ] 445 | }, 446 | { 447 | "long_name" : "Suffolk", 448 | "short_name" : "Suffolk", 449 | "types" : [ "administrative_area_level_2", "political" ] 450 | }, 451 | { 452 | "long_name" : "Massachusetts", 453 | "short_name" : "MA", 454 | "types" : [ "administrative_area_level_1", "political" ] 455 | }, 456 | { 457 | "long_name" : "United States", 458 | "short_name" : "US", 459 | "types" : [ "country", "political" ] 460 | }, 461 | { 462 | "long_name" : "02127", 463 | "short_name" : "02127", 464 | "types" : [ "postal_code" ] 465 | } 466 | ], 467 | "formatted_address" : "Hall Place, Boston, MA 02127, USA", 468 | "geometry" : { 469 | "bounds" : { 470 | "northeast" : { 471 | "lat" : 42.3339539, 472 | "lng" : -71.034485 473 | }, 474 | "southwest" : { 475 | "lat" : 42.3335792, 476 | "lng" : -71.0347695 477 | } 478 | }, 479 | "location" : { 480 | "lat" : 42.3336703, 481 | "lng" : -71.0347611 482 | }, 483 | "location_type" : "GEOMETRIC_CENTER", 484 | "viewport" : { 485 | "northeast" : { 486 | "lat" : 42.3351155302915, 487 | "lng" : -71.0332782697085 488 | }, 489 | "southwest" : { 490 | "lat" : 42.3324175697085, 491 | "lng" : -71.03597623029151 492 | } 493 | } 494 | }, 495 | "partial_match" : true, 496 | "types" : [ "route" ] 497 | }, 498 | { 499 | "address_components" : [ 500 | { 501 | "long_name" : "Hall Avenue", 502 | "short_name" : "Hall Ave", 503 | "types" : [ "route" ] 504 | }, 505 | { 506 | "long_name" : "Dudley / Brunswick King", 507 | "short_name" : "Dudley / Brunswick King", 508 | "types" : [ "neighborhood", "political" ] 509 | }, 510 | { 511 | "long_name" : "Roxbury", 512 | "short_name" : "Roxbury", 513 | "types" : [ "sublocality", "political" ] 514 | }, 515 | { 516 | "long_name" : "Boston", 517 | "short_name" : "Boston", 518 | "types" : [ "locality", "political" ] 519 | }, 520 | { 521 | "long_name" : "Suffolk", 522 | "short_name" : "Suffolk", 523 | "types" : [ "administrative_area_level_2", "political" ] 524 | }, 525 | { 526 | "long_name" : "Massachusetts", 527 | "short_name" : "MA", 528 | "types" : [ "administrative_area_level_1", "political" ] 529 | }, 530 | { 531 | "long_name" : "United States", 532 | "short_name" : "US", 533 | "types" : [ "country", "political" ] 534 | }, 535 | { 536 | "long_name" : "02121", 537 | "short_name" : "02121", 538 | "types" : [ "postal_code" ] 539 | } 540 | ], 541 | "formatted_address" : "Hall Avenue, Boston, MA 02121, USA", 542 | "geometry" : { 543 | "bounds" : { 544 | "northeast" : { 545 | "lat" : 42.3084888, 546 | "lng" : -71.082103 547 | }, 548 | "southwest" : { 549 | "lat" : 42.3082657, 550 | "lng" : -71.0821694 551 | } 552 | }, 553 | "location" : { 554 | "lat" : 42.30838, 555 | "lng" : -71.0821347 556 | }, 557 | "location_type" : "GEOMETRIC_CENTER", 558 | "viewport" : { 559 | "northeast" : { 560 | "lat" : 42.3097262302915, 561 | "lng" : -71.0807872197085 562 | }, 563 | "southwest" : { 564 | "lat" : 42.3070282697085, 565 | "lng" : -71.0834851802915 566 | } 567 | } 568 | }, 569 | "partial_match" : true, 570 | "types" : [ "route" ] 571 | } 572 | ], 573 | "status" : "OK" 574 | } 575 | -------------------------------------------------------------------------------- /app/assets/javascripts/main.js.erb: -------------------------------------------------------------------------------- 1 | $(function() { 2 | var center = new google.maps.LatLng(42.358431, -71.059773); 3 | var mapOptions = { 4 | center: center, 5 | disableDoubleClickZoom: true, 6 | keyboardShortcuts: false, 7 | mapTypeControl: false, 8 | mapTypeId: google.maps.MapTypeId.ROADMAP, 9 | maxZoom: 19, 10 | minZoom: 15, 11 | panControl: false, 12 | rotateControl: false, 13 | scaleControl: false, 14 | scrollwheel: false, 15 | streetViewControl: true, 16 | zoom: 15, 17 | zoomControl: true 18 | }; 19 | var map = new google.maps.Map(document.getElementById("map"), mapOptions); 20 | var size = new google.maps.Size(27.0, 37.0); 21 | var origin = new google.maps.Point(0, 0); 22 | var anchor = new google.maps.Point(13.0, 18.0); 23 | var greenMarkerImage = new google.maps.MarkerImage('<%= image_path 'markers/green.png' %>', 24 | size, 25 | origin, 26 | anchor 27 | ); 28 | var redMarkerImage = new google.maps.MarkerImage('<%= image_path 'markers/red.png' %>', 29 | size, 30 | origin, 31 | anchor 32 | ); 33 | var markerShadowImage = new google.maps.MarkerImage('<%= image_path 'markers/shadow.png' %>', 34 | new google.maps.Size(46.0, 37.0), 35 | origin, 36 | anchor 37 | ); 38 | var activeThingId; 39 | var activeMarker; 40 | var activeInfoWindow; 41 | var isWindowOpen = false; 42 | var thingIds = []; 43 | function addMarker(thingId, point, color) { 44 | if(color === 'green') { 45 | var image = greenMarkerImage; 46 | } else if(color === 'red') { 47 | var image = redMarkerImage; 48 | } 49 | var marker = new google.maps.Marker({ 50 | animation: google.maps.Animation.DROP, 51 | icon: image, 52 | map: map, 53 | position: point, 54 | shadow: markerShadowImage 55 | }); 56 | google.maps.event.addListener(marker, 'click', function() { 57 | if(activeInfoWindow) { 58 | activeInfoWindow.close(); 59 | } 60 | var infoWindow = new google.maps.InfoWindow({ 61 | maxWidth: 210 62 | }); 63 | google.maps.event.addListener(infoWindow, 'closeclick', function() { 64 | isWindowOpen = false; 65 | }); 66 | activeInfoWindow = infoWindow; 67 | activeThingId = thingId; 68 | activeMarker = marker; 69 | $.ajax({ 70 | type: 'GET', 71 | url: '/info_window', 72 | data: { 73 | 'thing_id': thingId 74 | }, 75 | success: function(data) { 76 | // Prevent race condition, which could lead to multiple windows being open at the same time. 77 | if(infoWindow === activeInfoWindow) { 78 | infoWindow.setContent(data); 79 | infoWindow.open(map, marker); 80 | isWindowOpen = true; 81 | } 82 | } 83 | }); 84 | }); 85 | thingIds.push(thingId); 86 | } 87 | function addMarkersAround(lat, lng) { 88 | var submitButton = $("#address_form input[type='submit']"); 89 | $.ajax({ 90 | type: 'GET', 91 | url: '/things.json', 92 | data: { 93 | 'utf8': '✓', 94 | 'lat': lat, 95 | 'lng': lng, 96 | 'limit': $('#address_form input[name="limit"]').val() 97 | }, 98 | error: function(jqXHR) { 99 | $(submitButton).attr("disabled", false); 100 | }, 101 | success: function(data) { 102 | $(submitButton).attr("disabled", false); 103 | if(data.errors) { 104 | $('#address').parent().addClass('error'); 105 | $('#address').focus(); 106 | } else { 107 | $('#address').parent().removeClass('error'); 108 | var i = -1; 109 | $(data).each(function(index, thing) { 110 | if($.inArray(thing.id, thingIds) === -1) { 111 | i += 1; 112 | } else { 113 | // continue 114 | return true; 115 | } 116 | setTimeout(function() { 117 | var point = new google.maps.LatLng(thing.lat, thing.lng); 118 | if(thing.user_id) { 119 | var color = 'green'; 120 | } else { 121 | var color = 'red'; 122 | } 123 | addMarker(thing.id, point, color); 124 | }, i * 100); 125 | }); 126 | } 127 | } 128 | }); 129 | } 130 | google.maps.event.addListener(map, 'idle', function() { 131 | var center = map.getCenter(); 132 | addMarkersAround(center.lat(), center.lng()); 133 | }); 134 | $('#address_form').live('submit', function() { 135 | var submitButton = $("#address_form input[type='submit']"); 136 | $(submitButton).attr("disabled", true); 137 | if($('#address').val() === '') { 138 | $(submitButton).attr("disabled", false); 139 | $('#address').parent().addClass('error'); 140 | $('#address').focus(); 141 | } else { 142 | $.ajax({ 143 | type: 'GET', 144 | url: '/address.json', 145 | data: { 146 | 'utf8': '✓', 147 | 'city_state': $('#city_state').val(), 148 | 'address': $('#address').val() 149 | }, 150 | error: function(jqXHR) { 151 | $(submitButton).attr("disabled", false); 152 | $('#address').parent().addClass('error'); 153 | $('#address').focus(); 154 | }, 155 | success: function(data) { 156 | $(submitButton).attr("disabled", false); 157 | if(data.errors) { 158 | $('#address').parent().addClass('error'); 159 | $('#address').focus(); 160 | } else { 161 | $('#address').parent().removeClass('error'); 162 | addMarkersAround(data[0], data[1]); 163 | var center = new google.maps.LatLng(data[0], data[1]); 164 | map.setCenter(center); 165 | map.setZoom(19); 166 | } 167 | } 168 | }); 169 | } 170 | return false; 171 | }); 172 | // Focus on the first non-empty text input or password field 173 | function setComboFormFocus() { 174 | $('#combo-form input[type="email"], #combo-form input[type="text"]:visible, #combo-form input[type="password"]:visible, #combo-form input[type="submit"]:visible, #combo-form input[type="tel"]:visible, #combo-form button:visible').each(function(index) { 175 | if($(this).val() === "" || $(this).attr('type') === 'submit' || this.tagName.toLowerCase() === 'button') { 176 | $(this).focus(); 177 | return false; 178 | } 179 | }); 180 | } 181 | $('#combo-form input[type="radio"]').live('click', function() { 182 | var radioInput = $(this); 183 | if('new' === radioInput.val()) { 184 | $('#combo-form').data('state', 'user_sign_up'); 185 | $('#user_forgot_password_fields').slideUp(); 186 | $('#user_sign_in_fields').slideUp(); 187 | $('#user_sign_up_fields').slideDown(function() { 188 | setComboFormFocus(); 189 | }); 190 | } else if('existing' === radioInput.val()) { 191 | $('#user_sign_up_fields').slideUp(); 192 | $('#user_sign_in_fields').slideDown(function() { 193 | $('#combo-form').data('state', 'user_sign_in'); 194 | setComboFormFocus(); 195 | $('#user_forgot_password_link').click(function() { 196 | $('#combo-form').data('state', 'user_forgot_password'); 197 | $('#user_sign_in_fields').slideUp(); 198 | $('#user_forgot_password_fields').slideDown(function() { 199 | setComboFormFocus(); 200 | $('#user_remembered_password_link').click(function() { 201 | $('#combo-form').data('state', 'user_sign_in'); 202 | $('#user_forgot_password_fields').slideUp(); 203 | $('#user_sign_in_fields').slideDown(function() { 204 | setComboFormFocus(); 205 | }); 206 | }); 207 | }); 208 | }); 209 | }); 210 | } 211 | }); 212 | $('#combo-form').live('submit', function() { 213 | var submitButton = $("#combo-form input[type='submit']"); 214 | $(submitButton).attr("disabled", true); 215 | var errors = [] 216 | if(!/[\w\.%\+]+@[\w]+\.+[\w]{2,}/.test($('#user_email').val())) { 217 | errors.push($('#user_email')); 218 | $('#user_email').parent().addClass('error'); 219 | } else { 220 | $('#user_email').parent().removeClass('error'); 221 | } 222 | if(!$(this).data('state') || $(this).data('state') === 'user_sign_up') { 223 | if($('#user_name').val() === '') { 224 | errors.push($('#user_name')); 225 | $('#user_name').parent().addClass('error'); 226 | } else { 227 | $('#user_name').parent().removeClass('error'); 228 | } 229 | if($('#user_password_confirmation').val().length < 6 || $('#user_password_confirmation').val().length > 20) { 230 | errors.push($('#user_password_confirmation')); 231 | $('#user_password_confirmation').parent().addClass('error'); 232 | } else { 233 | $('#user_password_confirmation').parent().removeClass('error'); 234 | } 235 | if(errors.length > 0) { 236 | $(submitButton).attr("disabled", false); 237 | errors[0].focus(); 238 | } else { 239 | $.ajax({ 240 | type: 'POST', 241 | url: '/users.json', 242 | data: { 243 | 'utf8': '✓', 244 | 'authenticity_token': $('#combo-form input[name="authenticity_token"]').val(), 245 | 'user': { 246 | 'email': $('#user_email').val(), 247 | 'name': $('#user_name').val(), 248 | 'organization': $('#user_organization').val(), 249 | 'voice_number': $('#user_voice_number').val(), 250 | 'sms_number': $('#user_sms_number').val(), 251 | 'password': $('#user_password_confirmation').val(), 252 | 'password_confirmation': $('#user_password_confirmation').val() 253 | } 254 | }, 255 | error: function(jqXHR) { 256 | var data = $.parseJSON(jqXHR.responseText); 257 | $(submitButton).attr("disabled", false); 258 | if(data.errors.email) { 259 | errors.push($('#user_email')); 260 | $('#user_email').parent().addClass('error'); 261 | } 262 | if(data.errors.name) { 263 | errors.push($('#user_name')); 264 | $('#user_name').parent().addClass('error'); 265 | } 266 | if(data.errors.organization) { 267 | errors.push($('#user_organization')); 268 | $('#user_organization').parent().addClass('error'); 269 | } 270 | if(data.errors.voice_number) { 271 | errors.push($('#user_voice_number')); 272 | $('#user_voice_number').parent().addClass('error'); 273 | } 274 | if(data.errors.sms_number) { 275 | errors.push($('#user_sms_number')); 276 | $('#user_sms_number').parent().addClass('error'); 277 | } 278 | if(data.errors.password) { 279 | errors.push($('#user_password_confirmation')); 280 | $('#user_password_confirmation').parent().addClass('error'); 281 | } 282 | errors[0].focus(); 283 | }, 284 | success: function(data) { 285 | $.ajax({ 286 | type: 'GET', 287 | url: '/sidebar/search', 288 | data: { 289 | 'flash': { 290 | 'notice': "<%= I18n.t("notices.signed_up") %>" 291 | } 292 | }, 293 | success: function(data) { 294 | $('#content').html(data); 295 | } 296 | }); 297 | } 298 | }); 299 | } 300 | } else if($(this).data('state') === 'user_sign_in') { 301 | if($('#user_password').val().length < 6 || $('#user_password').val().length > 20) { 302 | errors.push($('#user_password')); 303 | $('#user_password').parent().addClass('error'); 304 | } else { 305 | $('#user_password').parent().removeClass('error'); 306 | } 307 | if(errors.length > 0) { 308 | $(submitButton).attr("disabled", false); 309 | errors[0].focus(); 310 | } else { 311 | $.ajax({ 312 | type: 'POST', 313 | url: '/users/sign_in.json', 314 | data: { 315 | 'utf8': '✓', 316 | 'authenticity_token': $('#combo-form input[name="authenticity_token"]').val(), 317 | 'user': { 318 | 'email': $('#user_email').val(), 319 | 'password': $('#user_password').val(), 320 | 'remember_me': $('#user_remember_me').val() 321 | } 322 | }, 323 | error: function(jqXHR) { 324 | $(submitButton).attr("disabled", false); 325 | $('#user_password').parent().addClass('error'); 326 | $('#user_password').focus(); 327 | }, 328 | success: function(data) { 329 | $.ajax({ 330 | type: 'GET', 331 | url: '/sidebar/search', 332 | data: { 333 | 'flash': { 334 | 'notice': "<%= I18n.t("notices.signed_in") %>" 335 | } 336 | }, 337 | success: function(data) { 338 | $('#content').html(data); 339 | } 340 | }); 341 | } 342 | }); 343 | } 344 | } else if($(this).data('state') === 'user_forgot_password') { 345 | if(errors.length > 0) { 346 | $(submitButton).attr("disabled", false); 347 | errors[0].focus(); 348 | } else { 349 | $.ajax({ 350 | type: 'POST', 351 | url: '/users/password.json', 352 | data: { 353 | 'utf8': '✓', 354 | 'authenticity_token': $('#combo-form input[name="authenticity_token"]').val(), 355 | 'user': { 356 | 'email': $('#user_email').val() 357 | } 358 | }, 359 | error: function(jqXHR) { 360 | $(submitButton).attr("disabled", false); 361 | $('#user_email').parent().addClass('error'); 362 | $('#user_email').focus(); 363 | }, 364 | success: function() { 365 | $(submitButton).attr("disabled", false); 366 | $('#user_remembered_password_link').click(); 367 | $('#user_password').focus(); 368 | } 369 | }); 370 | } 371 | } 372 | return false; 373 | }); 374 | $('#adoption_form').live('submit', function() { 375 | var submitButton = $("#adoption_form input[type='submit']"); 376 | $(submitButton).attr("disabled", true); 377 | $.ajax({ 378 | type: 'POST', 379 | url: '/things.json', 380 | data: { 381 | '_method': 'patch', 382 | 'id': $('#thing_id').val(), 383 | 'utf8': '✓', 384 | 'authenticity_token': $('#adoption_form input[name="authenticity_token"]').val(), 385 | 'thing': { 386 | 'user_id': $('#thing_user_id').val(), 387 | 'name': $('#thing_name').val() 388 | } 389 | }, 390 | error: function(jqXHR) { 391 | $(submitButton).attr("disabled", false); 392 | }, 393 | success: function(data) { 394 | $.ajax({ 395 | type: 'GET', 396 | url: '/info_window', 397 | data: { 398 | 'thing_id': activeThingId, 399 | 'flash': { 400 | 'notice': "<%= I18n.t("notices.adopted", thing: I18n.t("defaults.thing")) %>" 401 | } 402 | }, 403 | success: function(data) { 404 | activeInfoWindow.close(); 405 | activeInfoWindow.setContent(data); 406 | activeInfoWindow.open(map, activeMarker); 407 | activeMarker.setIcon(greenMarkerImage); 408 | activeMarker.setAnimation(google.maps.Animation.BOUNCE); 409 | } 410 | }); 411 | } 412 | }); 413 | return false; 414 | }); 415 | $('#abandon_form').live('submit', function() { 416 | var answer = window.confirm("Are you sure you want to abandon this <%= I18n.t("defaults.thing") %>?") 417 | if(answer) { 418 | var submitButton = $("#abandon_form input[type='submit']"); 419 | $(submitButton).attr("disabled", true); 420 | $.ajax({ 421 | type: 'POST', 422 | url: '/things.json', 423 | data: { 424 | '_method': 'patch', 425 | 'id': $('#thing_id').val(), 426 | 'utf8': '✓', 427 | 'authenticity_token': $('#abandon_form input[name="authenticity_token"]').val(), 428 | 'thing': { 429 | 'user_id': $('#thing_user_id').val(), 430 | 'name': $('#thing_name').val() 431 | } 432 | }, 433 | error: function(jqXHR) { 434 | $(submitButton).attr("disabled", false); 435 | }, 436 | success: function(data) { 437 | $.ajax({ 438 | type: 'GET', 439 | url: '/info_window', 440 | data: { 441 | 'thing_id': activeThingId, 442 | 'flash': { 443 | 'warning': "<%= I18n.t("notices.abandoned", thing: I18n.t("defaults.thing").capitalize) %>" 444 | } 445 | }, 446 | success: function(data) { 447 | activeInfoWindow.close(); 448 | activeInfoWindow.setContent(data); 449 | activeInfoWindow.open(map, activeMarker); 450 | activeMarker.setIcon(redMarkerImage); 451 | activeMarker.setAnimation(null); 452 | } 453 | }); 454 | } 455 | }); 456 | } 457 | return false; 458 | }); 459 | $('#edit_profile_link').live('click', function() { 460 | var link = $(this); 461 | $(link).addClass('disabled'); 462 | $.ajax({ 463 | type: 'GET', 464 | url: '/users/edit', 465 | error: function(jqXHR) { 466 | $(link).removeClass('disabled'); 467 | }, 468 | success: function(data) { 469 | $('#content').html(data); 470 | } 471 | }); 472 | return false; 473 | }); 474 | $('#edit_form').live('submit', function() { 475 | var submitButton = $("#edit_form input[type='submit']"); 476 | $(submitButton).attr("disabled", true); 477 | var errors = [] 478 | if(!/[\w\.%\+\]+@[\w\]+\.+[\w]{2,}/.test($('#user_email').val())) { 479 | errors.push($('#user_email')); 480 | $('#user_email').parent().addClass('error'); 481 | } else { 482 | $('#user_email').parent().removeClass('error'); 483 | } 484 | if($('#user_name').val() === '') { 485 | errors.push($('#user_name')); 486 | $('#user_name').parent().addClass('error'); 487 | } else { 488 | $('#user_name').parent().removeClass('error'); 489 | } 490 | if($('#user_zip').val() != '' && !/^\d{5}(-\d{4})?$/.test($('#user_zip').val())) { 491 | errors.push($('#user_zip')); 492 | $('#user_zip').parent().addClass('error'); 493 | } else { 494 | $('#user_zip').parent().removeClass('error'); 495 | } 496 | if($('#user_password').val() && ($('#user_password').val().length < 6 || $('#user_password').val().length > 20)) { 497 | errors.push($('#user_password')); 498 | $('#user_password').parent().addClass('error'); 499 | } else { 500 | $('#user_password').parent().removeClass('error'); 501 | } 502 | if($('#user_current_password').val().length < 6 || $('#user_current_password').val().length > 20) { 503 | errors.push($('#user_current_password')); 504 | $('#user_current_password').parent().addClass('error'); 505 | } else { 506 | $('#user_current_password').parent().removeClass('error'); 507 | } 508 | if(errors.length > 0) { 509 | $(submitButton).attr("disabled", false); 510 | errors[0].focus(); 511 | } else { 512 | $.ajax({ 513 | type: 'POST', 514 | url: '/users.json', 515 | data: { 516 | '_method': 'patch', 517 | 'id': $('#id').val(), 518 | 'thing_id': activeThingId, 519 | 'utf8': '✓', 520 | 'authenticity_token': $('#edit_form input[name="authenticity_token"]').val(), 521 | 'user': { 522 | 'email': $('#user_email').val(), 523 | 'name': $('#user_name').val(), 524 | 'organization': $('#user_organization').val(), 525 | 'voice_number': $('#user_voice_number').val(), 526 | 'sms_number': $('#user_sms_number').val(), 527 | 'address_1': $('#user_address_1').val(), 528 | 'address_2': $('#user_address_2').val(), 529 | 'city': $('#user_city').val(), 530 | 'state': $('#user_state').val(), 531 | 'zip': $('#user_zip').val(), 532 | 'password': $('#user_password').val(), 533 | 'password_confirmation': $('#user_password').val(), 534 | 'current_password': $('#user_current_password').val() 535 | } 536 | }, 537 | error: function(jqXHR) { 538 | var data = $.parseJSON(jqXHR.responseText); 539 | $(submitButton).attr("disabled", false); 540 | if(data.errors.email) { 541 | errors.push($('#user_email')); 542 | $('#user_email').parent().addClass('error'); 543 | } 544 | if(data.errors.name) { 545 | errors.push($('#user_name')); 546 | $('#user_name').parent().addClass('error'); 547 | } 548 | if(data.errors.organization) { 549 | errors.push($('#user_organization')); 550 | $('#user_organization').parent().addClass('error'); 551 | } 552 | if(data.errors.voice_number) { 553 | errors.push($('#user_voice_number')); 554 | $('#user_voice_number').parent().addClass('error'); 555 | } 556 | if(data.errors.sms_number) { 557 | errors.push($('#user_sms_number')); 558 | $('#user_sms_number').parent().addClass('error'); 559 | } 560 | if(data.errors.address_1) { 561 | errors.push($('#user_address_1')); 562 | $('#user_address_1').parent().addClass('error'); 563 | } 564 | if(data.errors.address_2) { 565 | errors.push($('#user_address_2')); 566 | $('#user_address_2').parent().addClass('error'); 567 | } 568 | if(data.errors.city) { 569 | errors.push($('#user_city')); 570 | $('#user_city').parent().addClass('error'); 571 | } 572 | if(data.errors.state) { 573 | errors.push($('#user_state')); 574 | $('#user_state').parent().addClass('error'); 575 | } 576 | if(data.errors.zip) { 577 | errors.push($('#user_zip')); 578 | $('#user_zip').parent().addClass('error'); 579 | } 580 | if(data.errors.password) { 581 | errors.push($('#user_password')); 582 | $('#user_password').parent().addClass('error'); 583 | } 584 | if(data.errors.current_password) { 585 | errors.push($('#user_current_password')); 586 | $('#user_current_password').parent().addClass('error'); 587 | } 588 | errors[0].focus(); 589 | }, 590 | success: function(data) { 591 | $('#content').html(data); 592 | } 593 | }); 594 | } 595 | return false; 596 | }); 597 | $('#sign_out_link').live('click', function() { 598 | var link = $(this); 599 | $(link).addClass('disabled'); 600 | $.ajax({ 601 | type: 'DELETE', 602 | url: '/users/sign_out.json', 603 | error: function(jqXHR) { 604 | $(link).removeClass('disabled'); 605 | }, 606 | success: function(data) { 607 | $.ajax({ 608 | type: 'GET', 609 | url: '/sidebar/combo_form', 610 | data: { 611 | 'flash': { 612 | 'warning': "<%= I18n.t("notices.signed_out") %>" 613 | } 614 | }, 615 | success: function(data) { 616 | $('#content').html(data); 617 | } 618 | }); 619 | } 620 | }); 621 | return false; 622 | }); 623 | $('#sign_in_form').live('submit', function() { 624 | var submitButton = $("#sign_in_form input[type='submit']"); 625 | $(submitButton).attr("disabled", true); 626 | $.ajax({ 627 | type: 'GET', 628 | url: '/users/sign_in', 629 | error: function(jqXHR) { 630 | $(submitButton).attr("disabled", false); 631 | }, 632 | success: function(data) { 633 | activeInfoWindow.close(); 634 | activeInfoWindow.setContent(data); 635 | activeInfoWindow.open(map, activeMarker); 636 | } 637 | }); 638 | return false; 639 | }); 640 | $('#back_link').live('click', function() { 641 | var link = $(this); 642 | $(link).addClass('disabled'); 643 | $.ajax({ 644 | type: 'GET', 645 | url: '/sidebar/search', 646 | error: function(jqXHR) { 647 | $(link).removeClass('disabled'); 648 | }, 649 | success: function(data) { 650 | $('#content').html(data); 651 | } 652 | }); 653 | return false; 654 | }); 655 | $('#reminder_form').live('submit', function() { 656 | var submitButton = $("#reminder_form input[type='submit']"); 657 | $(submitButton).attr("disabled", true); 658 | $.ajax({ 659 | type: 'POST', 660 | url: '/reminders.json', 661 | data: { 662 | 'utf8': '✓', 663 | 'authenticity_token': $('#reminder_form input[name="authenticity_token"]').val(), 664 | 'reminder': { 665 | 'to_user_id': $('#reminder_to_user_id').val(), 666 | 'thing_id': activeThingId 667 | } 668 | }, 669 | error: function(jqXHR) { 670 | $(submitButton).attr("disabled", false); 671 | }, 672 | success: function(data) { 673 | $.ajax({ 674 | type: 'GET', 675 | url: '/info_window', 676 | data: { 677 | 'thing_id': activeThingId, 678 | 'flash': { 679 | 'notice': "<%= I18n.t("notices.reminder_sent") %>" 680 | } 681 | }, 682 | success: function(data) { 683 | activeInfoWindow.close(); 684 | activeInfoWindow.setContent(data); 685 | activeInfoWindow.open(map, activeMarker); 686 | } 687 | }); 688 | } 689 | }); 690 | return false; 691 | }); 692 | $('.alert-message').alert(); 693 | }); 694 | --------------------------------------------------------------------------------