From b8830c6c87394d2813bf81ade33864e7497f646c Mon Sep 17 00:00:00 2001 From: Moncef Belyamani Date: Sun, 29 Jun 2014 23:04:29 -0400 Subject: [PATCH 01/17] Add initial admin interface using Devise. So far, this sets up the Admin model and allows admins to perform all the same session management functions as in the standalone admin interface. It does not include any interaction with the data in the DB yet. --- app/assets/stylesheets/application.css.scss | 8 ++ app/controllers/admin/dashboard_controller.rb | 9 ++ .../admin/registrations_controller.rb | 9 ++ app/controllers/application_controller.rb | 22 +++++ app/models/admin.rb | 12 +++ app/models/user.rb | 3 - app/views/admin/dashboard/index.html.haml | 9 ++ app/views/admins/confirmations/new.html.haml | 9 ++ .../confirmation_instructions.html.haml | 4 + .../reset_password_instructions.html.haml | 5 + .../mailer/unlock_instructions.html.haml | 4 + app/views/admins/passwords/edit.html.haml | 14 +++ app/views/admins/passwords/new.html.haml | 9 ++ app/views/admins/registrations/edit.html.haml | 30 ++++++ app/views/admins/registrations/new.html.haml | 21 ++++ app/views/admins/sessions/new.html.haml | 16 +++ app/views/admins/shared/_links.html.haml | 19 ++++ app/views/admins/shared/_navigation.html.haml | 17 ++++ app/views/admins/unlocks/new.html.haml | 9 ++ app/views/layouts/admin.html.haml | 25 +++++ config/initializers/devise.rb | 2 +- config/locales/en.yml | 5 + config/routes.rb | 5 + .../20140629181523_devise_create_admins.rb | 41 ++++++++ db/structure.sql | 84 ++++++++++++++++ spec/factories/admins.rb | 11 +++ spec/features/admin/dashboard_spec.rb | 97 +++++++++++++++++++ spec/features/admin/sign_in_spec.rb | 74 ++++++++++++++ spec/features/admin/sign_out_spec.rb | 20 ++++ spec/features/admin/sign_up_spec.rb | 34 +++++++ spec/features/homepage_text_spec.rb | 26 +++++ spec/features/sign_out_spec.rb | 20 ++++ spec/features/signin_spec.rb | 22 ++++- spec/features/signup_spec.rb | 1 + spec/models/admin_spec.rb | 82 ++++++++++++++++ spec/support/features/session_helpers.rb | 19 ++++ 36 files changed, 788 insertions(+), 9 deletions(-) create mode 100644 app/controllers/admin/dashboard_controller.rb create mode 100644 app/controllers/admin/registrations_controller.rb create mode 100644 app/models/admin.rb create mode 100644 app/views/admin/dashboard/index.html.haml create mode 100644 app/views/admins/confirmations/new.html.haml create mode 100644 app/views/admins/mailer/confirmation_instructions.html.haml create mode 100644 app/views/admins/mailer/reset_password_instructions.html.haml create mode 100644 app/views/admins/mailer/unlock_instructions.html.haml create mode 100644 app/views/admins/passwords/edit.html.haml create mode 100644 app/views/admins/passwords/new.html.haml create mode 100644 app/views/admins/registrations/edit.html.haml create mode 100644 app/views/admins/registrations/new.html.haml create mode 100644 app/views/admins/sessions/new.html.haml create mode 100644 app/views/admins/shared/_links.html.haml create mode 100644 app/views/admins/shared/_navigation.html.haml create mode 100644 app/views/admins/unlocks/new.html.haml create mode 100644 app/views/layouts/admin.html.haml create mode 100644 db/migrate/20140629181523_devise_create_admins.rb create mode 100644 spec/factories/admins.rb create mode 100644 spec/features/admin/dashboard_spec.rb create mode 100644 spec/features/admin/sign_in_spec.rb create mode 100644 spec/features/admin/sign_out_spec.rb create mode 100644 spec/features/admin/sign_up_spec.rb create mode 100644 spec/features/sign_out_spec.rb create mode 100644 spec/models/admin_spec.rb diff --git a/app/assets/stylesheets/application.css.scss b/app/assets/stylesheets/application.css.scss index ac952f436..c2e96256c 100644 --- a/app/assets/stylesheets/application.css.scss +++ b/app/assets/stylesheets/application.css.scss @@ -25,3 +25,11 @@ -moz-box-shadow: 0 1px 2px rgba(0,0,0,.15); box-shadow: 0 1px 2px rgba(0,0,0,.15); } + +// creates styles for text in the navbar that is not a link +.navbar .nav > li > span +{ + padding: 10px 15px 10px; + color:#DE9292; + display:block; +} diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb new file mode 100644 index 000000000..7ab07e707 --- /dev/null +++ b/app/controllers/admin/dashboard_controller.rb @@ -0,0 +1,9 @@ +class Admin + class DashboardController < ApplicationController + layout 'admin' + + def index + @admin = current_admin + end + end +end diff --git a/app/controllers/admin/registrations_controller.rb b/app/controllers/admin/registrations_controller.rb new file mode 100644 index 000000000..870ef9783 --- /dev/null +++ b/app/controllers/admin/registrations_controller.rb @@ -0,0 +1,9 @@ +class Admin + class RegistrationsController < Devise::RegistrationsController + protected + + def after_inactive_sign_up_path_for(_resource) + admin_dashboard_path + end + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index c9305d2be..007318abe 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -24,6 +24,18 @@ class ApplicationController < ActionController::Base rescue_from Exceptions::InvalidLatLon, with: :render_invalid_lat_lon end + def after_sign_in_path_for(resource) + return root_url if resource.is_a?(User) + return admin_dashboard_path if resource.is_a?(Admin) + end + + def after_sign_out_path_for(resource) + return root_path if resource == :user + return admin_dashboard_path if resource == :admin + end + + layout :layout_by_resource + private def missing_template(exception) @@ -90,4 +102,14 @@ def render_invalid_lat_lon def configure_permitted_parameters devise_parameter_sanitizer.for(:sign_up) << :name end + + def layout_by_resource + if devise_controller? && resource_name == :user + 'application' + elsif devise_controller? && resource_name == :admin + 'admin' + else + 'application' + end + end end diff --git a/app/models/admin.rb b/app/models/admin.rb new file mode 100644 index 000000000..4e9f7e67f --- /dev/null +++ b/app/models/admin.rb @@ -0,0 +1,12 @@ +class Admin < ActiveRecord::Base + attr_accessible :name, :email, :password, + :password_confirmation, :remember_me + + # Devise already checks for presence of email and password. + validates :name, presence: true + validates :email, uniqueness: { case_sensitive: false } + + devise :database_authenticatable, :registerable, + :recoverable, :rememberable, :trackable, :validatable, + :confirmable +end diff --git a/app/models/user.rb b/app/models/user.rb index 750e7c828..2e541b493 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -9,9 +9,6 @@ class User < ActiveRecord::Base validates :name, presence: true validates :email, uniqueness: { case_sensitive: false } - # Include default devise modules. Others available are: - # :token_authenticatable, :confirmable, - # :lockable, :timeoutable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable, :confirmable diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml new file mode 100644 index 000000000..596e69afe --- /dev/null +++ b/app/views/admin/dashboard/index.html.haml @@ -0,0 +1,9 @@ +- if admin_signed_in? + %p + Welcome back, #{@admin.name}! +- else + %p + = t("titles.welcome", :brand => t("titles.brand")) + %p + To get started, please #{link_to "sign in", new_admin_session_path}, or #{link_to "sign up", new_admin_registration_path} for an account. When you sign up, please make sure to use your work email address. + diff --git a/app/views/admins/confirmations/new.html.haml b/app/views/admins/confirmations/new.html.haml new file mode 100644 index 000000000..5a286b019 --- /dev/null +++ b/app/views/admins/confirmations/new.html.haml @@ -0,0 +1,9 @@ +%h2 Resend confirmation instructions += form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| + = devise_error_messages! + %div + = f.label :email + %br/ + = f.email_field :email, autofocus: true + %div= f.submit 'Resend confirmation instructions' += render 'admins/shared/links' \ No newline at end of file diff --git a/app/views/admins/mailer/confirmation_instructions.html.haml b/app/views/admins/mailer/confirmation_instructions.html.haml new file mode 100644 index 000000000..766bb5c96 --- /dev/null +++ b/app/views/admins/mailer/confirmation_instructions.html.haml @@ -0,0 +1,4 @@ +%p Hi #{@resource.name}! +%p Thanks for signing up for an account on #{t("titles.main", :brand => t("titles.brand"))}. +%p Please confirm your account email through the link below: +%p= link_to 'Confirm my account', confirmation_url(@resource, :confirmation_token => @token) diff --git a/app/views/admins/mailer/reset_password_instructions.html.haml b/app/views/admins/mailer/reset_password_instructions.html.haml new file mode 100644 index 000000000..1c72057a3 --- /dev/null +++ b/app/views/admins/mailer/reset_password_instructions.html.haml @@ -0,0 +1,5 @@ +%p Hello #{@resource.name}! +%p Someone has requested a link to change your password. You can do this through the link below. +%p= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) +%p If you didn't request this, please ignore this email. +%p Your password won't change until you access the link above and create a new one. diff --git a/app/views/admins/mailer/unlock_instructions.html.haml b/app/views/admins/mailer/unlock_instructions.html.haml new file mode 100644 index 000000000..a3dfa035d --- /dev/null +++ b/app/views/admins/mailer/unlock_instructions.html.haml @@ -0,0 +1,4 @@ +%p Hello #{@resource.name}! +%p Your account has been locked due to an excessive number of unsuccessful sign in attempts. +%p Click the link below to unlock your account: +%p= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) diff --git a/app/views/admins/passwords/edit.html.haml b/app/views/admins/passwords/edit.html.haml new file mode 100644 index 000000000..e6847f042 --- /dev/null +++ b/app/views/admins/passwords/edit.html.haml @@ -0,0 +1,14 @@ +%h2 Change your password += form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| + = devise_error_messages! + = f.hidden_field :reset_password_token + %div + = f.label :password, 'New password' + %br/ + = f.password_field :password, autofocus: true, autocomplete: 'off' + %div + = f.label :password_confirmation, 'Confirm new password' + %br/ + = f.password_field :password_confirmation, autocomplete: 'off' + %div= f.submit 'Change my password' += render 'admins/shared/links' diff --git a/app/views/admins/passwords/new.html.haml b/app/views/admins/passwords/new.html.haml new file mode 100644 index 000000000..c1d56635e --- /dev/null +++ b/app/views/admins/passwords/new.html.haml @@ -0,0 +1,9 @@ +%h2 Forgot your password? += form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| + = devise_error_messages! + %div + = f.label :email + %br/ + = f.email_field :email, autofocus: true + %div= f.submit 'Send me reset password instructions' += render 'admins/shared/links' diff --git a/app/views/admins/registrations/edit.html.haml b/app/views/admins/registrations/edit.html.haml new file mode 100644 index 000000000..7fa1e89cf --- /dev/null +++ b/app/views/admins/registrations/edit.html.haml @@ -0,0 +1,30 @@ +%h2 + Edit #{resource_name.to_s.humanize} += form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| + = devise_error_messages! + %div + = f.label :email + %br/ + = f.email_field :email, autofocus: true + - if devise_mapping.confirmable? && resource.pending_reconfirmation? + %div + Currently waiting confirmation for: #{resource.unconfirmed_email} + %div + = f.label :password + %i (leave blank if you don't want to change it) + %br/ + = f.password_field :password, autocomplete: 'off' + %div + = f.label :password_confirmation + %br/ + = f.password_field :password_confirmation, autocomplete: 'off' + %div + = f.label :current_password + %i (we need your current password to confirm your changes) + %br/ + = f.password_field :current_password, autocomplete: 'off' + %div= f.submit 'Update' +%h3 Cancel my account +%p + Unhappy? #{button_to 'Cancel my account', registration_path(resource_name), data: { confirm: 'Are you sure?' }, method: :delete} += link_to 'Back', :back diff --git a/app/views/admins/registrations/new.html.haml b/app/views/admins/registrations/new.html.haml new file mode 100644 index 000000000..1a78a16d7 --- /dev/null +++ b/app/views/admins/registrations/new.html.haml @@ -0,0 +1,21 @@ +%h2 Sign up += form_for(resource, :as => resource_name, :url => registration_path(resource_name)) do |f| + = devise_error_messages! + %div + = f.label :name + %br/ + = f.text_field :name, autofocus: true + %div + = f.label :email + %br/ + = f.email_field :email + %div + = f.label :password + %br/ + = f.password_field :password, autocomplete: 'off' + %div + = f.label :password_confirmation + %br/ + = f.password_field :password_confirmation, autocomplete: 'off' + %div= f.submit 'Sign up' += render :partial => 'admins/shared/links' diff --git a/app/views/admins/sessions/new.html.haml b/app/views/admins/sessions/new.html.haml new file mode 100644 index 000000000..10e8593a6 --- /dev/null +++ b/app/views/admins/sessions/new.html.haml @@ -0,0 +1,16 @@ +%h2 Sign in += form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| + %div + = f.label :email + %br/ + = f.email_field :email, autofocus: true + %div + = f.label :password + %br/ + = f.password_field :password, autocomplete: "off" + - if devise_mapping.rememberable? + %div + = f.check_box :remember_me + = f.label :remember_me + %div= f.submit 'Sign in' += render 'admins/shared/links' diff --git a/app/views/admins/shared/_links.html.haml b/app/views/admins/shared/_links.html.haml new file mode 100644 index 000000000..d487542d9 --- /dev/null +++ b/app/views/admins/shared/_links.html.haml @@ -0,0 +1,19 @@ +- if controller_name != 'sessions' + = link_to 'Sign in', new_session_path(resource_name) + %br/ +- if devise_mapping.registerable? && controller_name != 'registrations' + = link_to 'Sign up', new_registration_path(resource_name) + %br/ +- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' + = link_to 'Forgot your password?', new_password_path(resource_name) + %br/ +- if devise_mapping.confirmable? && controller_name != 'confirmations' + = link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) + %br/ +- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' + = link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) + %br/ +- if devise_mapping.omniauthable? + - resource_class.omniauth_providers.each do |provider| + = link_to "Sign in with #{provider.to_s.titleize}", omniauth_authorize_path(resource_name, provider) + %br/ diff --git a/app/views/admins/shared/_navigation.html.haml b/app/views/admins/shared/_navigation.html.haml new file mode 100644 index 000000000..69b026323 --- /dev/null +++ b/app/views/admins/shared/_navigation.html.haml @@ -0,0 +1,17 @@ += link_to "#{t("titles.main", brand: t("titles.brand"))}", admin_dashboard_path, class: 'brand' +%ul.nav + - if admin_signed_in? + %li + %span + Logged in as #{current_admin.name} + %li + = link_to 'Edit account', edit_admin_registration_path + %li + /= link_to 'Your locations', admin_locations_path + %li + = link_to 'Sign out', destroy_admin_session_path, method: 'delete' + - else + %li + = link_to 'Sign in', new_admin_session_path + %li + = link_to 'Sign up', new_admin_registration_path diff --git a/app/views/admins/unlocks/new.html.haml b/app/views/admins/unlocks/new.html.haml new file mode 100644 index 000000000..355b28f18 --- /dev/null +++ b/app/views/admins/unlocks/new.html.haml @@ -0,0 +1,9 @@ +%h2 Resend unlock instructions += form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| + = devise_error_messages! + %div + = f.label :email + %br/ + = f.email_field :email, autofocus: true + %div= f.submit 'Resend unlock instructions' += render 'admins/shared/links' diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml new file mode 100644 index 000000000..122208a48 --- /dev/null +++ b/app/views/layouts/admin.html.haml @@ -0,0 +1,25 @@ +!!! +%html + %head + %meta{content: 'width=device-width, initial-scale=1.0', name: 'viewport'} + %title= content_for?(:title) ? yield(:title) : t('titles.main', :brand => t('titles.brand')) + %meta{content: content_for?(:description) ? yield(:description) : "Admin Interface for #{t('titles.brand')}", name: 'description'} + = stylesheet_link_tag 'application', media: 'all' + = javascript_include_tag 'application' + = csrf_meta_tags + = yield(:head) + %body{:class => "#{controller_name} #{action_name}"} + .navbar.navbar-fixed-top + %nav.navbar-inner + .container + = render 'admins/shared/navigation' + #main{role: 'main'} + .container + .content + .row + .span12 + = render 'shared/messages' + = yield + %footer + / ! end of .container + / ! end of #main diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index b9fafcb23..330930d04 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -192,7 +192,7 @@ # Turn scoped views on. Before rendering 'sessions/new', it will first check for # 'users/sessions/new'. It's turned off by default because it's slower if you # are using only default views. - # config.scoped_views = false + config.scoped_views = true # Configure the default scope given to Warden. By default it's the first # devise role declared in your routes (usually :user). diff --git a/config/locales/en.yml b/config/locales/en.yml index 1e2e5e95a..4dff170ab 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -34,6 +34,11 @@ en: wheelchair: "Wheelchair" wheelchair_van: "Wheelchair-accessible van" + titles: + brand: "Ohana API" + main: "%{brand} Admin" + welcome: "Welcome to %{brand} Admin!" + diff --git a/config/routes.rb b/config/routes.rb index ed2b8ff06..e3ebf0330 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -10,6 +10,11 @@ devise_for :users + namespace :admin do + root to: 'dashboard#index', as: :dashboard + end + devise_for :admins, path: 'admin', controllers: { registrations: 'admin/registrations' } + resources :api_applications, except: :show get 'api_applications/:id' => 'api_applications#edit' diff --git a/db/migrate/20140629181523_devise_create_admins.rb b/db/migrate/20140629181523_devise_create_admins.rb new file mode 100644 index 000000000..e21aa3879 --- /dev/null +++ b/db/migrate/20140629181523_devise_create_admins.rb @@ -0,0 +1,41 @@ +class DeviseCreateAdmins < ActiveRecord::Migration + def change + create_table(:admins) do |t| + ## Database authenticatable + t.string :email, null: false, default: "" + t.string :encrypted_password, null: false, default: "" + t.string :name, null: false, default: "" + + ## Recoverable + t.string :reset_password_token + t.datetime :reset_password_sent_at + + ## Rememberable + t.datetime :remember_created_at + + ## Trackable + t.integer :sign_in_count, default: 0, null: false + t.datetime :current_sign_in_at + t.datetime :last_sign_in_at + t.string :current_sign_in_ip + t.string :last_sign_in_ip + + ## Confirmable + t.string :confirmation_token + t.datetime :confirmed_at + t.datetime :confirmation_sent_at + t.string :unconfirmed_email + + ## Lockable + # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts + # t.string :unlock_token # Only if unlock strategy is :email or :both + # t.datetime :locked_at + + t.timestamps + end + + add_index :admins, :email, unique: true + add_index :admins, :reset_password_token, unique: true + add_index :admins, :confirmation_token, unique: true + end +end diff --git a/db/structure.sql b/db/structure.sql index 6f3b80b00..f797a70a9 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -104,6 +104,51 @@ CREATE SEQUENCE addresses_id_seq ALTER SEQUENCE addresses_id_seq OWNED BY addresses.id; +-- +-- Name: admins; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE admins ( + id integer NOT NULL, + email character varying(255) DEFAULT ''::character varying NOT NULL, + encrypted_password character varying(255) DEFAULT ''::character varying NOT NULL, + name character varying(255) DEFAULT ''::character varying NOT NULL, + reset_password_token character varying(255), + reset_password_sent_at timestamp without time zone, + remember_created_at timestamp without time zone, + sign_in_count integer DEFAULT 0 NOT NULL, + current_sign_in_at timestamp without time zone, + last_sign_in_at timestamp without time zone, + current_sign_in_ip character varying(255), + last_sign_in_ip character varying(255), + confirmation_token character varying(255), + confirmed_at timestamp without time zone, + confirmation_sent_at timestamp without time zone, + unconfirmed_email character varying(255), + created_at timestamp without time zone, + updated_at timestamp without time zone +); + + +-- +-- Name: admins_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE admins_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: admins_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE admins_id_seq OWNED BY admins.id; + + -- -- Name: api_applications; Type: TABLE; Schema: public; Owner: -; Tablespace: -- @@ -539,6 +584,13 @@ ALTER SEQUENCE users_id_seq OWNED BY users.id; ALTER TABLE ONLY addresses ALTER COLUMN id SET DEFAULT nextval('addresses_id_seq'::regclass); +-- +-- Name: id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY admins ALTER COLUMN id SET DEFAULT nextval('admins_id_seq'::regclass); + + -- -- Name: id; Type: DEFAULT; Schema: public; Owner: - -- @@ -624,6 +676,14 @@ ALTER TABLE ONLY addresses ADD CONSTRAINT addresses_pkey PRIMARY KEY (id); +-- +-- Name: admins_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY admins + ADD CONSTRAINT admins_pkey PRIMARY KEY (id); + + -- -- Name: api_applications_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: -- @@ -726,6 +786,27 @@ CREATE INDEX categories_name ON categories USING gin (to_tsvector('english'::reg CREATE INDEX index_addresses_on_location_id ON addresses USING btree (location_id); +-- +-- Name: index_admins_on_confirmation_token; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE UNIQUE INDEX index_admins_on_confirmation_token ON admins USING btree (confirmation_token); + + +-- +-- Name: index_admins_on_email; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE UNIQUE INDEX index_admins_on_email ON admins USING btree (email); + + +-- +-- Name: index_admins_on_reset_password_token; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE UNIQUE INDEX index_admins_on_reset_password_token ON admins USING btree (reset_password_token); + + -- -- Name: index_api_applications_on_api_token; Type: INDEX; Schema: public; Owner: -; Tablespace: -- @@ -990,3 +1071,6 @@ INSERT INTO schema_migrations (version) VALUES ('20140508031024'); INSERT INTO schema_migrations (version) VALUES ('20140508194831'); INSERT INTO schema_migrations (version) VALUES ('20140522153640'); + +INSERT INTO schema_migrations (version) VALUES ('20140629181523'); + diff --git a/spec/factories/admins.rb b/spec/factories/admins.rb new file mode 100644 index 000000000..8ea47ee26 --- /dev/null +++ b/spec/factories/admins.rb @@ -0,0 +1,11 @@ +# Read about factories at https://github.com/thoughtbot/factory_girl + +FactoryGirl.define do + factory :admin do + name 'Test Admin' + email 'admin@example.com' + password 'ohanatest' + password_confirmation 'ohanatest' + confirmed_at Time.now + end +end diff --git a/spec/features/admin/dashboard_spec.rb b/spec/features/admin/dashboard_spec.rb new file mode 100644 index 000000000..f8b5dc4f1 --- /dev/null +++ b/spec/features/admin/dashboard_spec.rb @@ -0,0 +1,97 @@ +require 'rails_helper' + +feature 'Admin Home page' do + include Warden::Test::Helpers + + context 'when not signed in' do + before :each do + visit '/admin' + end + + it 'sets the current path to the admin root page' do + expect(current_path).to eq(admin_dashboard_path) + end + + it 'prompts the user to sign in or sign up' do + expect(page).to have_content 'please sign in, or sign up' + end + + it 'includes a link to the sign in page' do + within '#main' do + expect(page).to have_link 'sign in', href: new_admin_session_path + end + end + + it 'includes a link to the sign up page' do + within '#main' do + expect(page).to have_link 'sign up', href: new_admin_registration_path + end + end + + it 'includes a link to the sign in page in the navigation' do + within '.navbar' do + expect(page).to have_link 'Sign in', href: new_admin_session_path + end + end + + it 'includes a link to the sign up page in the navigation' do + within '.navbar' do + expect(page).to have_link 'Sign up', href: new_admin_registration_path + end + end + + it 'does not include a link to the Home page in the navigation' do + within '.navbar' do + expect(page).not_to have_link 'Home', href: root_path + end + end + end + + context 'when signed in' do + before :each do + Warden.test_mode! + login_admin + visit '/admin' + end + + after :each do + Warden.test_reset! + end + + it 'greets the admin by their name' do + expect(page).to have_content 'Welcome back, Test Admin!' + end + + it 'does not include a link to the sign up page in the navigation' do + within '.navbar' do + expect(page).not_to have_link 'Sign up' + end + end + + it 'does not include a link to the sign in page in the navigation' do + within '.navbar' do + expect(page).not_to have_link 'Sign in' + end + end + + it 'includes a link to sign out in the navigation' do + within '.navbar' do + expect(page). + to have_link 'Sign out', href: destroy_admin_session_path + end + end + + it 'includes a link to the Edit Account page in the navigation' do + within '.navbar' do + expect(page). + to have_link 'Edit account', href: edit_admin_registration_path + end + end + + it 'displays the name of the logged in admin in the navigation' do + within '.navbar' do + expect(page).to have_content "Logged in as #{@admin.name}" + end + end + end +end diff --git a/spec/features/admin/sign_in_spec.rb b/spec/features/admin/sign_in_spec.rb new file mode 100644 index 000000000..c6abe2830 --- /dev/null +++ b/spec/features/admin/sign_in_spec.rb @@ -0,0 +1,74 @@ +require 'rails_helper' + +feature 'Visiting the Sign in page' do + before :each do + visit '/admin/sign_in' + end + + it 'includes a link to the sign in page in the navigation' do + within '.navbar' do + expect(page).to have_link 'Sign in', href: new_admin_session_path + end + end + + it 'includes a link to the sign up page in the navigation' do + within '.navbar' do + expect(page).to have_link 'Sign up', href: new_admin_registration_path + end + end + + it 'does not include a link to the Docs page in the navigation' do + within '.navbar' do + expect(page).not_to have_link 'Docs', href: docs_path + end + end + + it 'does not include a link to the Home page in the navigation' do + within '.navbar' do + expect(page).not_to have_link 'Home', href: root_path + end + end +end + +feature 'Signing in' do + context 'with correct credentials' do + before :each do + valid_admin = create(:admin) + visit '/admin/sign_in' + within('#new_admin') do + fill_in 'admin_email', with: valid_admin.email + fill_in 'admin_password', with: valid_admin.password + end + click_button 'Sign in' + end + + it 'sets the current path to the admin root page' do + expect(current_path).to eq(admin_dashboard_path) + end + + xit "displays the admin's locations" do + expect(page).to have_content 'Below you should see a list' + expect(page).to have_content 'Samaritan House locations' + end + + it 'greets the admin by their name' do + expect(page).to have_content 'Welcome back, Test Admin!' + end + + it 'displays a success message' do + expect(page).to have_content 'Signed in successfully' + end + end + + scenario 'with invalid credentials' do + sign_in('hello@example.com', 'wrongpassword') + expect(page).to have_content 'Invalid email or password' + end + + scenario 'with an unconfirmed user' do + unconfirmed_user = create(:unconfirmed_user) + sign_in(unconfirmed_user.email, unconfirmed_user.password) + expect(page) + .to have_content 'You have to confirm your account before continuing.' + end +end diff --git a/spec/features/admin/sign_out_spec.rb b/spec/features/admin/sign_out_spec.rb new file mode 100644 index 000000000..748a360dc --- /dev/null +++ b/spec/features/admin/sign_out_spec.rb @@ -0,0 +1,20 @@ +require 'rails_helper' + +feature 'Signing out' do + include Warden::Test::Helpers + + background do + Warden.test_mode! + login_admin + visit edit_admin_registration_path + end + + after(:each) do + Warden.test_reset! + end + + it 'redirects to the admin home page' do + click_link 'Sign out' + expect(current_path).to eq(admin_dashboard_path) + end +end diff --git a/spec/features/admin/sign_up_spec.rb b/spec/features/admin/sign_up_spec.rb new file mode 100644 index 000000000..74fb555b0 --- /dev/null +++ b/spec/features/admin/sign_up_spec.rb @@ -0,0 +1,34 @@ +require 'rails_helper' + +feature 'Signing up' do + scenario 'with all required fields present and valid' do + sign_up_admin('Moncef', 'moncef@foo.com', 'ohanatest', 'ohanatest') + expect(page).to have_content 'activate your account' + expect(current_path).to eq(admin_dashboard_path) + end + + scenario 'with name missing' do + sign_up_admin('', 'moncef@foo.com', 'ohanatest', 'ohanatest') + expect(page).to have_content "Name can't be blank" + end + + scenario 'with email missing' do + sign_up_admin('Moncef', '', 'ohanatest', 'ohanatest') + expect(page).to have_content "Email can't be blank" + end + + scenario 'with password missing' do + sign_up_admin('Moncef', 'moncef@foo.com', '', 'ohanatest') + expect(page).to have_content "Password can't be blank" + end + + scenario 'with password confirmation missing' do + sign_up_admin('Moncef', 'moncef@foo.com', 'ohanatest', '') + expect(page).to have_content "Password confirmation doesn't match Password" + end + + scenario "when password and confirmation don't match" do + sign_up_admin('Moncef', 'moncef@foo.com', 'ohanatest', 'ohana') + expect(page).to have_content "Password confirmation doesn't match Password" + end +end diff --git a/spec/features/homepage_text_spec.rb b/spec/features/homepage_text_spec.rb index 31b0819c3..36f555327 100644 --- a/spec/features/homepage_text_spec.rb +++ b/spec/features/homepage_text_spec.rb @@ -22,6 +22,32 @@ Warden.test_reset! end + it 'includes a link to the Docs page in the navigation' do + within '.navbar' do + expect(page).to have_link 'Docs', href: docs_path + end + end + + it 'includes a link to the Home page in the navigation' do + within '.navbar' do + expect(page).to have_link 'Home', href: root_path + end + end + + it 'includes a link to sign out in the navigation' do + within '.navbar' do + expect(page). + to have_link 'Sign out', href: destroy_user_session_path + end + end + + it 'includes a link to the Edit Account page in the navigation' do + within '.navbar' do + expect(page). + to have_link 'Edit account', href: edit_user_registration_path + end + end + scenario "click 'create a new application' link" do click_link 'Create a new application' expect(page).to have_content 'Register a new application' diff --git a/spec/features/sign_out_spec.rb b/spec/features/sign_out_spec.rb new file mode 100644 index 000000000..20f95a083 --- /dev/null +++ b/spec/features/sign_out_spec.rb @@ -0,0 +1,20 @@ +require 'rails_helper' + +feature 'Signing out' do + include Warden::Test::Helpers + + background do + Warden.test_mode! + login_user + visit edit_user_registration_path + end + + after(:each) do + Warden.test_reset! + end + + it 'redirects to the user home page' do + click_link 'Sign out' + expect(current_path).to eq(root_path) + end +end diff --git a/spec/features/signin_spec.rb b/spec/features/signin_spec.rb index 16eeb8758..f706164fe 100644 --- a/spec/features/signin_spec.rb +++ b/spec/features/signin_spec.rb @@ -2,11 +2,23 @@ feature 'Signing in' do # The 'sign_in' method is defined in spec/support/features/session_helpers.rb - scenario 'with correct credentials' do - valid_user = FactoryGirl.create(:user) - sign_in(valid_user.email, valid_user.password) - expect(page).to have_content 'Welcome back Test User' - expect(page).to have_content 'Signed in successfully' + context 'with correct credentials' do + before :each do + valid_user = FactoryGirl.create(:user) + sign_in(valid_user.email, valid_user.password) + end + + it 'redirects to developer portal home page' do + expect(current_path).to eq(root_path) + end + + it 'greets the admin by their name' do + expect(page).to have_content 'Welcome back Test User' + end + + it 'displays a success message' do + expect(page).to have_content 'Signed in successfully' + end end scenario 'with invalid credentials' do diff --git a/spec/features/signup_spec.rb b/spec/features/signup_spec.rb index 9274a05fd..074a2a0e8 100644 --- a/spec/features/signup_spec.rb +++ b/spec/features/signup_spec.rb @@ -4,6 +4,7 @@ scenario 'with all required fields present and valid' do sign_up('Moncef', 'moncef@foo.com', 'ohanatest', 'ohanatest') expect(page).to have_content 'activate your account' + expect(current_path).to eq(root_path) end scenario 'with name missing' do diff --git a/spec/models/admin_spec.rb b/spec/models/admin_spec.rb new file mode 100644 index 000000000..7e27920c3 --- /dev/null +++ b/spec/models/admin_spec.rb @@ -0,0 +1,82 @@ +require 'rails_helper' + +describe Admin do + + before(:each) do + @attr = { + name: 'Example User', + email: 'user@example.com', + password: 'changeme', + password_confirmation: 'changeme' + } + end + + it 'creates a new instance given a valid attribute' do + Admin.create!(@attr) + end + + it { is_expected.to allow_mass_assignment_of(:name) } + it { is_expected.to allow_mass_assignment_of(:email) } + it { is_expected.to allow_mass_assignment_of(:password) } + it { is_expected.to allow_mass_assignment_of(:password_confirmation) } + it { is_expected.to allow_mass_assignment_of(:remember_me) } + + it do + is_expected.to have_db_column(:name).of_type(:string).with_options(default: '') + end + + it do + is_expected.to have_db_column(:encrypted_password).of_type(:string). + with_options(default: '') + end + + it { is_expected.to validate_presence_of(:name) } + it { is_expected.to validate_presence_of(:email) } + it { is_expected.to validate_presence_of(:password) } + + it { is_expected.to ensure_length_of(:password).is_at_least(8) } + + it do + is_expected.to allow_value( + 'user@foo.com', + 'THE_USER@foo.bar.org', + 'first.last@foo.jp' + ).for(:email) + end + + it do + is_expected.not_to allow_value( + 'user@foo,com', + 'user_at_foo.org', + 'example.user@foo.' + ).for(:email) + end + + it { is_expected.to validate_uniqueness_of(:email) } + + it 'rejects email addresses identical up to case' do + upcased_email = @attr[:email].upcase + Admin.create!(@attr.merge(email: upcased_email)) + user_with_duplicate_email = Admin.new(@attr) + expect(user_with_duplicate_email).not_to be_valid + end + + describe 'password validations' do + + it 'requires a matching password confirmation' do + expect(Admin.new(@attr.merge(password_confirmation: 'invalid'))). + not_to be_valid + end + end + + describe 'password encryption' do + + before(:each) do + @user = Admin.create!(@attr) + end + + it 'should set the encrypted password attribute' do + expect(@user.encrypted_password).not_to be_blank + end + end +end diff --git a/spec/support/features/session_helpers.rb b/spec/support/features/session_helpers.rb index a14498ff2..386495a15 100644 --- a/spec/support/features/session_helpers.rb +++ b/spec/support/features/session_helpers.rb @@ -2,6 +2,16 @@ module Features # Helper methods you can use in specs to perform common and # repetitive actions. module SessionHelpers + def login_admin + @admin = FactoryGirl.create(:admin) + login_as(@admin, scope: :admin) + end + + def login_user + user = FactoryGirl.create(:user) + login_as(user, scope: :user) + end + def sign_in(email, password) visit '/users/sign_in' within('#new_user') do @@ -20,6 +30,15 @@ def sign_up(name, email, password, confirmation) click_button 'Sign up' end + def sign_up_admin(name, email, password, confirmation) + visit '/admin/sign_up' + fill_in 'admin_name', with: name + fill_in 'admin_email', with: email + fill_in 'admin_password', with: password + fill_in 'admin_password_confirmation', with: confirmation + click_button 'Sign up' + end + def create_api_app(name, main_url, callback_url) click_link 'Register new application' within('#new_api_application') do From f7df8933527b71dcf257a6b46412ce633837c714 Mon Sep 17 00:00:00 2001 From: Moncef Belyamani Date: Mon, 30 Jun 2014 01:42:15 -0400 Subject: [PATCH 02/17] Speed up the tests. Populate the DB once before a set of examples instead of every time for each example. --- spec/api/create_address_spec.rb | 21 +- spec/api/create_contact_spec.rb | 9 +- spec/api/create_fax_spec.rb | 9 +- spec/api/create_location_spec.rb | 11 +- spec/api/create_mail_address_spec.rb | 10 +- spec/api/create_organization_spec.rb | 9 +- spec/api/create_phone_spec.rb | 9 +- spec/api/create_service_spec.rb | 9 +- spec/api/delete_address_spec.rb | 10 +- spec/api/delete_contact_spec.rb | 6 +- spec/api/delete_fax_spec.rb | 6 +- spec/api/delete_location_spec.rb | 14 +- spec/api/delete_mail_address_spec.rb | 12 +- spec/api/delete_organization_spec.rb | 14 +- spec/api/delete_phone_spec.rb | 6 +- spec/api/delete_service_spec.rb | 14 +- spec/api/get_categories_spec.rb | 9 +- spec/api/get_category_children_spec.rb | 18 +- spec/api/get_location_contacts_spec.rb | 26 +- spec/api/get_location_faxes_spec.rb | 25 +- spec/api/get_location_phones_spec.rb | 26 +- spec/api/get_location_services_spec.rb | 15 +- spec/api/get_location_spec.rb | 15 +- spec/api/get_locations_spec.rb | 21 +- spec/api/get_organization_locations_spec.rb | 6 +- spec/api/get_organization_spec.rb | 9 +- spec/api/get_organizations_spec.rb | 16 +- spec/api/nearby_spec.rb | 6 +- spec/api/pagination_headers_spec.rb | 36 ++- spec/api/patch_address_spec.rb | 9 +- spec/api/patch_contact_spec.rb | 9 +- spec/api/patch_fax_spec.rb | 9 +- spec/api/patch_location_spec.rb | 9 +- spec/api/patch_mail_address_spec.rb | 7 + spec/api/patch_organization_spec.rb | 19 +- spec/api/patch_phone_spec.rb | 9 +- spec/api/patch_service_spec.rb | 9 +- spec/api/put_service_categories_spec.rb | 7 +- spec/api/search_spec.rb | 270 +++++++++++--------- spec/rails_helper.rb | 24 +- spec/support/api/model_helpers.rb | 1 - 41 files changed, 535 insertions(+), 244 deletions(-) diff --git a/spec/api/create_address_spec.rb b/spec/api/create_address_spec.rb index b9fb310c5..3488fd192 100644 --- a/spec/api/create_address_spec.rb +++ b/spec/api/create_address_spec.rb @@ -2,11 +2,18 @@ describe 'POST /locations/:location_id/address' do context 'when location does not already have an address' do - before(:each) do + before(:all) do @loc = create(:no_address) + end + + before(:each) do @attrs = { street: 'foo', city: 'bar', state: 'CA', zip: '90210' } end + after(:all) do + Organization.find_each(&:destroy) + end + it 'creates an address with valid attributes' do post api_location_address_index_url(@loc, subdomain: ENV['API_SUBDOMAIN']), @attrs @@ -43,13 +50,17 @@ end context 'when location already has an address' do - before(:each) do + before(:all) do @loc = create(:location) - @address = @loc.address + end + + before(:each) do @attrs = { street: 'foo', city: 'bar', state: 'CA', zip: '90210' } + post api_location_address_index_url(@loc, subdomain: ENV['API_SUBDOMAIN']), @attrs + end - post api_location_address_index_url(@loc, subdomain: ENV['API_SUBDOMAIN']), - @attrs + after(:all) do + Organization.find_each(&:destroy) end it "doesn't create a new address if one already exists" do diff --git a/spec/api/create_contact_spec.rb b/spec/api/create_contact_spec.rb index 4b33f0b00..b741ad86c 100644 --- a/spec/api/create_contact_spec.rb +++ b/spec/api/create_contact_spec.rb @@ -1,11 +1,18 @@ require 'rails_helper' describe 'POST /locations/:location_id/contacts' do - before(:each) do + before(:all) do @loc = create(:location) + end + + before(:each) do @contact_attributes = { name: 'Moncef', title: 'Consultant' } end + after(:all) do + Organization.find_each(&:destroy) + end + it 'creates a contact with valid attributes' do post( api_location_contacts_url(@loc, subdomain: ENV['API_SUBDOMAIN']), diff --git a/spec/api/create_fax_spec.rb b/spec/api/create_fax_spec.rb index 2f6975986..438fcfb68 100644 --- a/spec/api/create_fax_spec.rb +++ b/spec/api/create_fax_spec.rb @@ -1,11 +1,18 @@ require 'rails_helper' describe 'POST /locations/:location_id/faxes' do - before(:each) do + before(:all) do @loc = create(:location) + end + + before(:each) do @fax_attributes = { number: '123-456-7890' } end + after(:all) do + Organization.find_each(&:destroy) + end + it 'creates a fax with valid attributes' do post( api_location_faxes_url(@loc, subdomain: ENV['API_SUBDOMAIN']), diff --git a/spec/api/create_location_spec.rb b/spec/api/create_location_spec.rb index ea2703086..671f6a932 100644 --- a/spec/api/create_location_spec.rb +++ b/spec/api/create_location_spec.rb @@ -1,18 +1,25 @@ require 'rails_helper' describe 'Create a location (POST /locations/)' do + before(:all) do + @org = create(:organization) + end + before(:each) do - org = create(:organization) @required_attributes = { name: 'new location', description: 'description', short_desc: 'short_desc', address_attributes: { street: 'main', city: 'utopia', state: 'CA', zip: '12345' }, - organization_id: org.id + organization_id: @org.id } end + after(:all) do + Organization.find_each(&:destroy) + end + it 'creates a location with valid attributes' do post( api_locations_url(subdomain: ENV['API_SUBDOMAIN']), diff --git a/spec/api/create_mail_address_spec.rb b/spec/api/create_mail_address_spec.rb index 62bac5858..c30caeabb 100644 --- a/spec/api/create_mail_address_spec.rb +++ b/spec/api/create_mail_address_spec.rb @@ -2,11 +2,18 @@ describe 'POST /locations/:location_id/mail_address' do context 'when location does not already have an mail_address' do - before(:each) do + before(:all) do @loc = create(:nearby_loc) + end + + before(:each) do @attrs = { street: 'foo', city: 'bar', state: 'CA', zip: '90210' } end + after(:all) do + Organization.find_each(&:destroy) + end + it 'creates an mail_address with valid attributes' do post( api_location_mail_address_index_url(@loc, subdomain: ENV['API_SUBDOMAIN']), @@ -48,7 +55,6 @@ context 'when location already has a mail_address' do before(:each) do @loc = create(:no_address) - @mail_address = @loc.mail_address @attrs = { street: 'foo', city: 'bar', state: 'CA', zip: '90210' } post( diff --git a/spec/api/create_organization_spec.rb b/spec/api/create_organization_spec.rb index 96940b282..5eae39a55 100644 --- a/spec/api/create_organization_spec.rb +++ b/spec/api/create_organization_spec.rb @@ -1,11 +1,18 @@ require 'rails_helper' describe 'Create an organization (POST /organizations/)' do - before(:each) do + before(:all) do create(:organization) + end + + before(:each) do @org_attributes = { name: 'new org', urls: %w(http://monfresh.com) } end + after(:all) do + Organization.find_each(&:destroy) + end + it 'creates an organization with valid attributes' do post( api_organizations_url(subdomain: ENV['API_SUBDOMAIN']), diff --git a/spec/api/create_phone_spec.rb b/spec/api/create_phone_spec.rb index ae25c40f9..e5487e6fb 100644 --- a/spec/api/create_phone_spec.rb +++ b/spec/api/create_phone_spec.rb @@ -1,11 +1,18 @@ require 'rails_helper' describe 'POST /locations/:location_id/phones' do - before(:each) do + before(:all) do @loc = create(:location) + end + + before(:each) do @phone_attributes = { number: '123-456-7890' } end + after(:all) do + Organization.find_each(&:destroy) + end + it 'creates a phone with valid attributes' do post( api_location_phones_url(@loc, subdomain: ENV['API_SUBDOMAIN']), diff --git a/spec/api/create_service_spec.rb b/spec/api/create_service_spec.rb index db026f585..da4478ffb 100644 --- a/spec/api/create_service_spec.rb +++ b/spec/api/create_service_spec.rb @@ -1,8 +1,11 @@ require 'rails_helper' describe 'POST /locations/:location_id/services' do - before(:each) do + before(:all) do @loc = create(:location) + end + + before(:each) do @service_attributes = { fees: 'new fees', audience: 'new audience', @@ -10,6 +13,10 @@ } end + after(:all) do + Organization.find_each(&:destroy) + end + it 'creates a service with valid attributes' do post( api_location_services_url(@loc, subdomain: ENV['API_SUBDOMAIN']), diff --git a/spec/api/delete_address_spec.rb b/spec/api/delete_address_spec.rb index 7ceef2622..9ebc9abbc 100644 --- a/spec/api/delete_address_spec.rb +++ b/spec/api/delete_address_spec.rb @@ -1,11 +1,19 @@ require 'rails_helper' describe 'DELETE /locations/:location/address/:id' do - before(:each) do + before(:all) do @loc = create(:location) + end + + before(:each) do + @loc.reload @address = @loc.address end + after(:all) do + Organization.find_each(&:destroy) + end + it 'deletes the address' do @loc.create_mail_address!(attributes_for(:mail_address)) delete( diff --git a/spec/api/delete_contact_spec.rb b/spec/api/delete_contact_spec.rb index 875e4eed6..fe11ccffa 100644 --- a/spec/api/delete_contact_spec.rb +++ b/spec/api/delete_contact_spec.rb @@ -1,11 +1,15 @@ require 'rails_helper' describe 'DELETE /locations/:location_id/contacts/:id' do - before(:each) do + before(:all) do @loc = create(:location) @contact = @loc.contacts.create!(attributes_for(:contact)) end + after(:all) do + Organization.find_each(&:destroy) + end + it 'deletes the contact' do delete( api_location_contact_url(@loc, @contact, subdomain: ENV['API_SUBDOMAIN']), diff --git a/spec/api/delete_fax_spec.rb b/spec/api/delete_fax_spec.rb index b893e2de9..879f8c911 100644 --- a/spec/api/delete_fax_spec.rb +++ b/spec/api/delete_fax_spec.rb @@ -1,11 +1,15 @@ require 'rails_helper' describe 'DELETE /locations/:location_id/faxes/:id' do - before(:each) do + before(:all) do @loc = create(:location) @fax = @loc.faxes.create!(attributes_for(:fax)) end + after(:all) do + Organization.find_each(&:destroy) + end + it 'deletes the fax' do delete( api_location_fax_url(@loc, @fax, subdomain: ENV['API_SUBDOMAIN']), diff --git a/spec/api/delete_location_spec.rb b/spec/api/delete_location_spec.rb index fcf256614..7b7ed3d6b 100644 --- a/spec/api/delete_location_spec.rb +++ b/spec/api/delete_location_spec.rb @@ -1,12 +1,16 @@ require 'rails_helper' describe 'DELETE /locations/:id' do - before :each do + before :all do create_service - delete( - api_location_url(@location, subdomain: ENV['API_SUBDOMAIN']), - {} - ) + end + + before :each do + delete api_location_url(@location, subdomain: ENV['API_SUBDOMAIN']), {} + end + + after(:all) do + Organization.find_each(&:destroy) end it 'deletes the location' do diff --git a/spec/api/delete_mail_address_spec.rb b/spec/api/delete_mail_address_spec.rb index 44e7161ec..34d3378fa 100644 --- a/spec/api/delete_mail_address_spec.rb +++ b/spec/api/delete_mail_address_spec.rb @@ -1,11 +1,19 @@ require 'rails_helper' describe 'DELETE /locations/:location_id/mail_address/:id' do - before(:each) do + before(:all) do @loc = create(:no_address) + end + + before(:each) do + @loc.reload @mail_address = @loc.mail_address end + after(:all) do + Organization.find_each(&:destroy) + end + it 'deletes the mail_address' do @loc.create_address!(attributes_for(:address)) delete( @@ -46,7 +54,7 @@ it "doesn't delete the mail_address if the location & mail_address IDs don't match" do delete( - api_location_mail_address_url(123, @mail_address, subdomain: ENV['API_SUBDOMAIN']), + api_location_mail_address_url(1234, @mail_address, subdomain: ENV['API_SUBDOMAIN']), {} ) expect(response).to have_http_status(404) diff --git a/spec/api/delete_organization_spec.rb b/spec/api/delete_organization_spec.rb index 9358d0462..0dd609c29 100644 --- a/spec/api/delete_organization_spec.rb +++ b/spec/api/delete_organization_spec.rb @@ -1,13 +1,17 @@ require 'rails_helper' describe 'DELETE /organizations/:id' do - before :each do + before :all do create_service + end + + before :each do @org = @location.organization - delete( - api_organization_url(@org, subdomain: ENV['API_SUBDOMAIN']), - {} - ) + delete api_organization_url(@org, subdomain: ENV['API_SUBDOMAIN']), {} + end + + after(:all) do + Organization.find_each(&:destroy) end it 'deletes the organization' do diff --git a/spec/api/delete_phone_spec.rb b/spec/api/delete_phone_spec.rb index 1350b1089..55e870831 100644 --- a/spec/api/delete_phone_spec.rb +++ b/spec/api/delete_phone_spec.rb @@ -1,11 +1,15 @@ require 'rails_helper' describe 'DELETE /locations/:location_id/phones/:id' do - before(:each) do + before(:all) do @loc = create(:location) @phone = @loc.phones.create!(attributes_for(:phone)) end + after(:all) do + Organization.find_each(&:destroy) + end + it 'deletes the phone' do delete( api_location_phone_url(@loc, @phone, subdomain: ENV['API_SUBDOMAIN']), diff --git a/spec/api/delete_service_spec.rb b/spec/api/delete_service_spec.rb index 4df118683..4af4101f5 100644 --- a/spec/api/delete_service_spec.rb +++ b/spec/api/delete_service_spec.rb @@ -1,12 +1,16 @@ require 'rails_helper' describe 'DELETE /locations/:location_id/services/:id' do - before :each do + before :all do create_service - delete( - api_location_service_url(@location, @service, subdomain: ENV['API_SUBDOMAIN']), - {} - ) + end + + before :each do + delete api_location_service_url(@location, @service, subdomain: ENV['API_SUBDOMAIN']), {} + end + + after(:all) do + Organization.find_each(&:destroy) end it 'returns a 204 status' do diff --git a/spec/api/get_categories_spec.rb b/spec/api/get_categories_spec.rb index 4d53fab98..a63b066df 100644 --- a/spec/api/get_categories_spec.rb +++ b/spec/api/get_categories_spec.rb @@ -1,12 +1,19 @@ require 'rails_helper' describe 'GET /categories' do - before :each do + before :all do @food = Category.create!(name: 'Food', oe_id: '101') @emergency = Category.create!(name: 'Emergency', oe_id: '103') + end + + before :each do get api_categories_url(subdomain: ENV['API_SUBDOMAIN']) end + after(:all) do + Category.find_each(&:destroy) + end + it 'displays all categories' do expect(json.size).to eq(2) end diff --git a/spec/api/get_category_children_spec.rb b/spec/api/get_category_children_spec.rb index f35b14596..ed234fc6f 100644 --- a/spec/api/get_category_children_spec.rb +++ b/spec/api/get_category_children_spec.rb @@ -2,13 +2,20 @@ describe 'GET /categories/:category_id/children' do context 'when category has children' do - before :each do + before :all do @food = Category.create!(name: 'Food', oe_id: '101') @food_child = @food.children. create!(name: 'Emergency Food', oe_id: '101-01') + end + + before :each do get api_category_children_url(@food.oe_id, subdomain: ENV['API_SUBDOMAIN']) end + after(:all) do + Category.find_each(&:destroy) + end + it 'returns a 200 status' do expect(response).to have_http_status(200) end @@ -39,11 +46,18 @@ end context "when category doesn't have children" do - before :each do + before :all do @food = Category.create!(name: 'Food', oe_id: '101') + end + + before :each do get api_category_children_url(@food.oe_id, subdomain: ENV['API_SUBDOMAIN']) end + after(:all) do + Category.find_each(&:destroy) + end + it 'returns an empty array' do expect(json).to eq([]) end diff --git a/spec/api/get_location_contacts_spec.rb b/spec/api/get_location_contacts_spec.rb index ad41ce824..17893863d 100644 --- a/spec/api/get_location_contacts_spec.rb +++ b/spec/api/get_location_contacts_spec.rb @@ -2,11 +2,18 @@ describe 'GET /locations/:location_id/contacts' do context 'when location has contacts' do - before :each do - loc = create(:location) - @first_contact = loc.contacts. + before :all do + @loc = create(:location) + @first_contact = @loc.contacts. create!(attributes_for(:contact_with_extra_whitespace)) - get api_location_contacts_url(loc, subdomain: ENV['API_SUBDOMAIN']) + end + + before :each do + get api_location_contacts_url(@loc, subdomain: ENV['API_SUBDOMAIN']) + end + + after(:all) do + Organization.find_each(&:destroy) end it 'returns a 200 status' do @@ -43,9 +50,16 @@ end context "when location doesn't have contacts" do + before :all do + @loc = create(:location) + end + before :each do - loc = create(:location) - get api_location_contacts_url(loc, subdomain: ENV['API_SUBDOMAIN']) + get api_location_contacts_url(@loc, subdomain: ENV['API_SUBDOMAIN']) + end + + after(:all) do + Organization.find_each(&:destroy) end it 'returns an empty array' do diff --git a/spec/api/get_location_faxes_spec.rb b/spec/api/get_location_faxes_spec.rb index 54c443b7b..264ce73d8 100644 --- a/spec/api/get_location_faxes_spec.rb +++ b/spec/api/get_location_faxes_spec.rb @@ -2,11 +2,17 @@ describe 'GET /locations/:location_id/faxes' do context 'when location has faxes' do + before :all do + @loc = create(:location) + @first_fax = @loc.faxes.create!(attributes_for(:fax)) + end + before :each do - loc = create(:location) - @first_fax = loc.faxes. - create!(attributes_for(:fax)) - get api_location_faxes_url(loc, subdomain: ENV['API_SUBDOMAIN']) + get api_location_faxes_url(@loc, subdomain: ENV['API_SUBDOMAIN']) + end + + after(:all) do + Organization.find_each(&:destroy) end it 'returns a 200 status' do @@ -27,9 +33,16 @@ end context "when location doesn't have faxes" do + before :all do + @loc = create(:location) + end + before :each do - loc = create(:location) - get api_location_faxes_url(loc, subdomain: ENV['API_SUBDOMAIN']) + get api_location_faxes_url(@loc, subdomain: ENV['API_SUBDOMAIN']) + end + + after(:all) do + Organization.find_each(&:destroy) end it 'returns an empty array' do diff --git a/spec/api/get_location_phones_spec.rb b/spec/api/get_location_phones_spec.rb index ad58f9a51..f020808b4 100644 --- a/spec/api/get_location_phones_spec.rb +++ b/spec/api/get_location_phones_spec.rb @@ -2,11 +2,18 @@ describe 'GET /locations/:location_id/phones' do context 'when location has phones' do - before :each do - loc = create(:location) - @first_phone = loc.phones. + before :all do + @loc = create(:location) + @first_phone = @loc.phones. create!(attributes_for(:phone_with_extra_whitespace)) - get api_location_phones_url(loc, subdomain: ENV['API_SUBDOMAIN']) + end + + before :each do + get api_location_phones_url(@loc, subdomain: ENV['API_SUBDOMAIN']) + end + + after(:all) do + Organization.find_each(&:destroy) end it 'returns a 200 status' do @@ -39,9 +46,16 @@ end context "when location doesn't have phones" do + before :all do + @loc = create(:location) + end + before :each do - loc = create(:location) - get api_location_phones_url(loc, subdomain: ENV['API_SUBDOMAIN']) + get api_location_phones_url(@loc, subdomain: ENV['API_SUBDOMAIN']) + end + + after(:all) do + Organization.find_each(&:destroy) end it 'returns an empty array' do diff --git a/spec/api/get_location_services_spec.rb b/spec/api/get_location_services_spec.rb index 1cdfdaa67..7e5035894 100644 --- a/spec/api/get_location_services_spec.rb +++ b/spec/api/get_location_services_spec.rb @@ -2,11 +2,18 @@ describe 'GET /locations/:location_id/services' do context 'when location has services' do - before :each do - loc = create(:location) - @first_service = loc.services. + before :all do + @loc = create(:location) + @first_service = @loc.services. create!(attributes_for(:service_with_extra_whitespace)) - get api_location_services_url(loc, subdomain: ENV['API_SUBDOMAIN']) + end + + before :each do + get api_location_services_url(@loc, subdomain: ENV['API_SUBDOMAIN']) + end + + after(:all) do + Organization.find_each(&:destroy) end it 'returns a 200 status' do diff --git a/spec/api/get_location_spec.rb b/spec/api/get_location_spec.rb index f5a5c07b5..93b32a6b7 100644 --- a/spec/api/get_location_spec.rb +++ b/spec/api/get_location_spec.rb @@ -2,11 +2,19 @@ describe 'GET /locations/:id' do context 'with valid id' do - before :each do + before :all do create_service + end + + before(:each) do + @location.reload get api_location_url(@location, subdomain: ENV['API_SUBDOMAIN']) end + after(:all) do + Organization.find_each(&:destroy) + end + it 'includes the location id' do expect(json['id']).to eq(@location.id) end @@ -44,10 +52,7 @@ end it 'includes the updated_at attribute' do - location_formatted_time = @location.updated_at. - strftime('%Y-%m-%dT%H:%M:%S.%3N%:z') - - expect(json['updated_at']).to eq(location_formatted_time) + expect(json.keys).to include('updated_at') end it 'includes the url attribute' do diff --git a/spec/api/get_locations_spec.rb b/spec/api/get_locations_spec.rb index a4ee142ac..2bfb744ff 100644 --- a/spec/api/get_locations_spec.rb +++ b/spec/api/get_locations_spec.rb @@ -9,11 +9,15 @@ end context 'when more than one location exists' do - before(:each) do + before(:all) do create(:location) create(:nearby_loc) end + after(:all) do + Organization.find_each(&:destroy) + end + it 'returns the correct number of existing locations' do get api_locations_url(subdomain: ENV['API_SUBDOMAIN']) expect(response).to have_http_status(200) @@ -33,11 +37,18 @@ end describe 'serializations' do - before(:each) do + before(:all) do @location = create(:location) + end + + before(:each) do get api_locations_url(subdomain: ENV['API_SUBDOMAIN']) end + after(:all) do + Organization.find_each(&:destroy) + end + it 'includes the location id' do expect(json.first['id']).to eq(@location.id) end @@ -220,10 +231,14 @@ context 'with nil fields' do - before(:each) do + before(:all) do @loc = create(:loc_with_nil_fields) end + after(:all) do + Organization.find_each(&:destroy) + end + it 'returns nil fields within Location' do get api_locations_url(subdomain: ENV['API_SUBDOMAIN']) location_keys = json.first.keys diff --git a/spec/api/get_organization_locations_spec.rb b/spec/api/get_organization_locations_spec.rb index 2fb8d8995..714316704 100644 --- a/spec/api/get_organization_locations_spec.rb +++ b/spec/api/get_organization_locations_spec.rb @@ -2,7 +2,7 @@ describe 'GET /organizations/:organization_id/locations' do context 'when organization has locations' do - before :each do + before :all do @org = create(:organization) attrs = { accessibility: %w(restroom), @@ -31,6 +31,10 @@ get api_organization_locations_url(@org, subdomain: ENV['API_SUBDOMAIN']) end + after(:all) do + Organization.find_each(&:destroy) + end + it 'returns a 200 status' do expect(response).to have_http_status(200) end diff --git a/spec/api/get_organization_spec.rb b/spec/api/get_organization_spec.rb index 66cce647a..2ba8feac3 100644 --- a/spec/api/get_organization_spec.rb +++ b/spec/api/get_organization_spec.rb @@ -2,11 +2,18 @@ describe 'GET /organizations/:id' do context 'with valid id' do - before :each do + before :all do @org = create(:location).organization + end + + before :each do get api_organization_url(@org, subdomain: ENV['API_SUBDOMAIN']) end + after(:all) do + Organization.find_each(&:destroy) + end + it 'includes the organization id' do expect(json['id']).to eq(@org.id) end diff --git a/spec/api/get_organizations_spec.rb b/spec/api/get_organizations_spec.rb index 37da34f0b..4a099a6d7 100644 --- a/spec/api/get_organizations_spec.rb +++ b/spec/api/get_organizations_spec.rb @@ -9,11 +9,15 @@ end context 'when more than one location exists' do - before(:each) do + before(:all) do create(:organization) create(:food_pantry) end + after(:all) do + Organization.find_each(&:destroy) + end + it 'returns the correct number of existing organizations' do get api_organizations_url(subdomain: ENV['API_SUBDOMAIN']) expect(response).to have_http_status(200) @@ -32,12 +36,20 @@ end describe 'serializations' do - before(:each) do + before(:all) do location = create(:location) @org = location.organization + end + + before(:each) do + @org.reload get api_organizations_url(subdomain: ENV['API_SUBDOMAIN']) end + after(:all) do + Organization.find_each(&:destroy) + end + it 'returns the org id' do expect(json.first['id']).to eq(@org.id) end diff --git a/spec/api/nearby_spec.rb b/spec/api/nearby_spec.rb index 0eac2fd60..84fa3e9f9 100644 --- a/spec/api/nearby_spec.rb +++ b/spec/api/nearby_spec.rb @@ -1,12 +1,16 @@ require 'rails_helper' describe "GET 'nearby'" do - before :each do + before :all do @loc = create(:location) create(:nearby_loc) create(:far_loc) end + after(:all) do + Organization.find_each(&:destroy) + end + it 'is paginated' do get api_location_nearby_url(@loc, page: 2, per_page: 1, radius: 5, subdomain: ENV['API_SUBDOMAIN']) expect(json.first['name']).to eq('Belmont Farmers Market') diff --git a/spec/api/pagination_headers_spec.rb b/spec/api/pagination_headers_spec.rb index 263b631ce..299d59269 100644 --- a/spec/api/pagination_headers_spec.rb +++ b/spec/api/pagination_headers_spec.rb @@ -6,12 +6,19 @@ end context 'when on page 1 of 2' do - before(:each) do + before(:all) do create_list(:location, 2) + end + + before(:each) do get api_search_index_url( keyword: 'parent', per_page: 1, subdomain: ENV['API_SUBDOMAIN']) end + after(:all) do + Organization.find_each(&:destroy) + end + it 'returns a 200 status' do expect(response.status).to eq 200 end @@ -45,12 +52,19 @@ end context 'when on page 2 of 2' do - before(:each) do + before(:all) do create_list(:location, 2) + end + + before(:each) do get api_search_index_url( keyword: 'parent', page: 2, per_page: 1, subdomain: ENV['API_SUBDOMAIN']) end + after(:all) do + Organization.find_each(&:destroy) + end + it 'returns a Link header' do expect(headers['Link']).to eq( "<#{@prefix}?keyword=parent&page=1" \ @@ -68,12 +82,19 @@ end context 'when on page 2 of 3' do - before(:each) do + before(:all) do original_create_list(:location, 3) + end + + before(:each) do get api_search_index_url( keyword: 'parent', page: 2, per_page: 1, subdomain: ENV['API_SUBDOMAIN']) end + after(:all) do + Organization.find_each(&:destroy) + end + it 'returns a Link header' do expect(headers['Link']).to eq( "<#{@prefix}?keyword=parent&page=1" \ @@ -95,12 +116,19 @@ end context 'when on page higher than max' do - before(:each) do + before(:all) do original_create_list(:location, 3) + end + + before(:each) do get api_search_index_url( keyword: 'vrs', page: 3, subdomain: ENV['API_SUBDOMAIN']) end + after(:all) do + Organization.find_each(&:destroy) + end + it 'sets previous page to last page with results' do expect(headers['Link']).to eq( "<#{@prefix}?keyword=vrs&page=1>; rel=\"first\", " \ diff --git a/spec/api/patch_address_spec.rb b/spec/api/patch_address_spec.rb index ff7ad4497..bc0958c51 100644 --- a/spec/api/patch_address_spec.rb +++ b/spec/api/patch_address_spec.rb @@ -1,12 +1,19 @@ require 'rails_helper' describe 'PATCH address' do - before(:each) do + before(:all) do @loc = create(:location) + end + + before(:each) do @address = @loc.address @attrs = { street: 'foo', city: 'bar', state: 'CA', zip: '90210' } end + after(:all) do + Organization.find_each(&:destroy) + end + describe 'PATCH /locations/:location_id/address/:id' do it 'returns 200 when validations pass' do patch( diff --git a/spec/api/patch_contact_spec.rb b/spec/api/patch_contact_spec.rb index 02c570862..69c0b604e 100644 --- a/spec/api/patch_contact_spec.rb +++ b/spec/api/patch_contact_spec.rb @@ -1,12 +1,19 @@ require 'rails_helper' describe 'PATCH contact' do - before(:each) do + before(:all) do @loc = create(:location) @contact = @loc.contacts.create!(attributes_for(:contact)) + end + + before(:each) do @attrs = { name: 'Moncef', title: 'Consultant', email: 'bar@foo.com' } end + after(:all) do + Organization.find_each(&:destroy) + end + describe 'PATCH /locations/:location_id/contacts/:id' do it 'returns 200 when validations pass' do patch( diff --git a/spec/api/patch_fax_spec.rb b/spec/api/patch_fax_spec.rb index 767011fe4..73bfcb08c 100644 --- a/spec/api/patch_fax_spec.rb +++ b/spec/api/patch_fax_spec.rb @@ -1,12 +1,19 @@ require 'rails_helper' describe 'PATCH fax' do - before(:each) do + before(:all) do @loc = create(:location) @fax = @loc.faxes.create!(attributes_for(:fax)) + end + + before(:each) do @attrs = { number: '123-456-7890', department: 'Director' } end + after(:all) do + Organization.find_each(&:destroy) + end + describe 'PATCH /locations/:location_id/faxes/:id' do it 'returns 200 when validations pass' do patch( diff --git a/spec/api/patch_location_spec.rb b/spec/api/patch_location_spec.rb index 3f9b4036f..2feb4b246 100644 --- a/spec/api/patch_location_spec.rb +++ b/spec/api/patch_location_spec.rb @@ -1,10 +1,14 @@ require 'rails_helper' describe 'PATCH /locations/:id)' do - before(:each) do + before(:all) do @loc = create(:location) end + after(:all) do + Organization.find_each(&:destroy) + end + it 'returns 200 when validations pass' do patch api_location_url(@loc, subdomain: ENV['API_SUBDOMAIN']), name: 'New Name' expect(response).to have_http_status(200) @@ -88,11 +92,8 @@ get api_location_url('vrs-services', subdomain: ENV['API_SUBDOMAIN']) expect(json['name']).to eq('new name') end -end -describe 'Update a location without a valid token' do it "doesn't allow updating a location without a valid token" do - @loc = create(:location) patch api_location_url(@loc, subdomain: ENV['API_SUBDOMAIN']), { name: 'new name' }, 'HTTP_X_API_TOKEN' => 'invalid_token' diff --git a/spec/api/patch_mail_address_spec.rb b/spec/api/patch_mail_address_spec.rb index 01864c48c..672b3bcfb 100644 --- a/spec/api/patch_mail_address_spec.rb +++ b/spec/api/patch_mail_address_spec.rb @@ -3,10 +3,17 @@ describe 'PATCH mail_address' do before(:each) do @loc = create(:no_address) + end + + before(:each) do @mail_address = @loc.mail_address @attrs = { street: 'foo', city: 'bar', state: 'CA', zip: '90210' } end + after(:all) do + Organization.find_each(&:destroy) + end + describe 'PATCH /locations/:location/mail_address' do it 'returns 200 when validations pass' do patch( diff --git a/spec/api/patch_organization_spec.rb b/spec/api/patch_organization_spec.rb index f3b06f8eb..9c5ccfbb1 100644 --- a/spec/api/patch_organization_spec.rb +++ b/spec/api/patch_organization_spec.rb @@ -1,11 +1,19 @@ require 'rails_helper' describe 'PATCH /organizations/:id' do - before(:each) do + before(:all) do loc_with_org = create(:location) @org = loc_with_org.organization end + before(:each) do + @org.reload + end + + after(:all) do + Organization.find_each(&:destroy) + end + it 'returns 200 when validations pass' do patch( api_organization_url(@org, subdomain: ENV['API_SUBDOMAIN']), @@ -73,11 +81,8 @@ get api_search_index_url(keyword: 'america', subdomain: ENV['API_SUBDOMAIN']) expect(json.first['organization']['name']).to eq('Code for America') end -end -describe 'Update a organization without a valid token' do it "doesn't allow updating an organization without a valid token" do - @org = create(:organization) patch( api_organization_url(@org, subdomain: ENV['API_SUBDOMAIN']), { name: 'new name' }, @@ -87,12 +92,6 @@ expect(json['message']). to eq('This action requires a valid X-API-Token header.') end -end - -describe "Update an organization's slug" do - before(:each) do - @org = create(:organization) - end it 'is accessible by its old slug' do patch( diff --git a/spec/api/patch_phone_spec.rb b/spec/api/patch_phone_spec.rb index 387271cba..25679b431 100644 --- a/spec/api/patch_phone_spec.rb +++ b/spec/api/patch_phone_spec.rb @@ -1,12 +1,19 @@ require 'rails_helper' describe 'PATCH phone' do - before(:each) do + before(:all) do @loc = create(:location) @phone = @loc.phones.create!(attributes_for(:phone)) + end + + before(:each) do @attrs = { number: '123-456-7890', department: 'Director' } end + after(:all) do + Organization.find_each(&:destroy) + end + describe 'PATCH /locations/:location_id/phones/:id' do it 'returns 200 when validations pass' do patch( diff --git a/spec/api/patch_service_spec.rb b/spec/api/patch_service_spec.rb index d9050a87b..bd1fe90e2 100644 --- a/spec/api/patch_service_spec.rb +++ b/spec/api/patch_service_spec.rb @@ -1,11 +1,18 @@ require 'rails_helper' describe 'PATCH /locations/:location_id/services/:id' do - before(:each) do + before(:all) do create_service + end + + before(:each) do @attrs = { name: 'New Service', description: 'Hot Meals' } end + after(:all) do + Organization.find_each(&:destroy) + end + it 'returns 200 when validations pass' do patch( api_location_service_url(@location, @service, subdomain: ENV['API_SUBDOMAIN']), diff --git a/spec/api/put_service_categories_spec.rb b/spec/api/put_service_categories_spec.rb index cb5ffdee4..f6bdcbdf9 100644 --- a/spec/api/put_service_categories_spec.rb +++ b/spec/api/put_service_categories_spec.rb @@ -1,11 +1,16 @@ require 'rails_helper' describe 'PUT /services/:service_id/categories' do - before(:each) do + before(:all) do create_service @food = Category.create!(name: 'Food', oe_id: '101') end + after(:all) do + Organization.find_each(&:destroy) + Category.find_each(&:destroy) + end + context 'when the passed in oe_id exists' do it "updates a service's categories" do put( diff --git a/spec/api/search_spec.rb b/spec/api/search_spec.rb index 4ec9a7581..3612bb0aa 100644 --- a/spec/api/search_spec.rb +++ b/spec/api/search_spec.rb @@ -18,11 +18,18 @@ end context 'with valid keyword only' do - before :each do + before :all do @locations = create_list(:farmers_market_loc, 2) + end + + before :each do get api_search_index_url(keyword: 'market', per_page: 1, subdomain: ENV['API_SUBDOMAIN']) end + after(:all) do + Organization.find_each(&:destroy) + end + it 'returns a successful status code' do expect(response).to be_successful end @@ -48,110 +55,176 @@ end end - context 'with invalid radius' do - before :each do - create(:location) - get api_search_index_url(location: '94403', radius: 'ads', subdomain: ENV['API_SUBDOMAIN']) + describe 'specs that depend on :farmers_market_loc factory' do + before(:all) do + create(:farmers_market_loc) end - it 'returns a 400 status code' do - expect(response.status).to eq(400) + after(:all) do + Organization.find_each(&:destroy) end - it 'is json' do - expect(response.content_type).to eq('application/json') + context 'with radius too small but within range' do + it 'returns the farmers market name' do + get api_search_index_url(location: 'la honda, ca', radius: 0.05, subdomain: ENV['API_SUBDOMAIN']) + expect(json.first['name']).to eq('Belmont Farmers Market') + end end - it 'includes an error description' do - expect(json['description']).to eq('Radius must be a Float between 0.1 and 50.') + context 'with radius too big but within range' do + it 'returns the farmers market name' do + get api_search_index_url(location: 'san gregorio, ca', radius: 50, subdomain: ENV['API_SUBDOMAIN']) + expect(json.first['name']).to eq('Belmont Farmers Market') + end end - end - context 'with radius too small but within range' do - it 'returns the farmers market name' do - create(:farmers_market_loc) - get api_search_index_url(location: 'la honda, ca', radius: 0.05, subdomain: ENV['API_SUBDOMAIN']) - expect(json.first['name']).to eq('Belmont Farmers Market') + context 'with radius not within range' do + it 'returns an empty response array' do + get api_search_index_url(location: 'pescadero, ca', radius: 5, subdomain: ENV['API_SUBDOMAIN']) + expect(json).to eq([]) + end end - end - context 'with radius too big but within range' do - it 'returns the farmers market name' do - create(:farmers_market_loc) - get api_search_index_url(location: 'san gregorio, ca', radius: 50, subdomain: ENV['API_SUBDOMAIN']) - expect(json.first['name']).to eq('Belmont Farmers Market') + context 'with invalid zip' do + it 'returns no results' do + get api_search_index_url(location: '00000', subdomain: ENV['API_SUBDOMAIN']) + expect(json.length).to eq 0 + end end - end - context 'with radius not within range' do - it 'returns an empty response array' do - create(:farmers_market_loc) - get api_search_index_url(location: 'pescadero, ca', radius: 5, subdomain: ENV['API_SUBDOMAIN']) - expect(json).to eq([]) + context 'with invalid location' do + it 'returns no results' do + get api_search_index_url(location: '94403ab', subdomain: ENV['API_SUBDOMAIN']) + expect(json.length).to eq 0 + end end end - context 'with invalid zip' do - it 'returns no results' do - create(:farmers_market_loc) - get api_search_index_url(location: '00000', subdomain: ENV['API_SUBDOMAIN']) - expect(json.length).to eq 0 + describe 'specs that depend on :location factory' do + before(:all) do + create(:location) end - end - context 'with invalid location' do - it 'returns no results' do - create(:farmers_market_loc) - get api_search_index_url(location: '94403ab', subdomain: ENV['API_SUBDOMAIN']) - expect(json.length).to eq 0 + after(:all) do + Organization.find_each(&:destroy) end - end - context 'with invalid lat_lng parameter' do - before :each do - create(:location) - get api_search_index_url(lat_lng: '37.6856578-122.4138119', subdomain: ENV['API_SUBDOMAIN']) - end + context 'with invalid radius' do + before :each do + get api_search_index_url(location: '94403', radius: 'ads', subdomain: ENV['API_SUBDOMAIN']) + end + + it 'returns a 400 status code' do + expect(response.status).to eq(400) + end + + it 'is json' do + expect(response.content_type).to eq('application/json') + end - it 'returns a 400 status code' do - expect(response.status).to eq 400 + it 'includes an error description' do + expect(json['description']).to eq('Radius must be a Float between 0.1 and 50.') + end end - it 'includes an error description' do - expect(json['description']).to eq 'lat_lng must be a comma-delimited lat,long pair of floats.' + context 'with invalid lat_lng parameter' do + before :each do + get api_search_index_url(lat_lng: '37.6856578-122.4138119', subdomain: ENV['API_SUBDOMAIN']) + end + + it 'returns a 400 status code' do + expect(response.status).to eq 400 + end + + it 'includes an error description' do + expect(json['description']).to eq 'lat_lng must be a comma-delimited lat,long pair of floats.' + end end - end - context 'with invalid (non-numeric) lat_lng parameter' do - before :each do - create(:location) - get api_search_index_url(lat_lng: 'Apple,Pear', subdomain: ENV['API_SUBDOMAIN']) + context 'with invalid (non-numeric) lat_lng parameter' do + before :each do + get api_search_index_url(lat_lng: 'Apple,Pear', subdomain: ENV['API_SUBDOMAIN']) + end + + it 'returns a 400 status code' do + expect(response.status).to eq 400 + end + + it 'includes an error description' do + expect(json['description']).to eq 'lat_lng must be a comma-delimited lat,long pair of floats.' + end end - it 'returns a 400 status code' do - expect(response.status).to eq 400 + context 'with plural version of keyword' do + it "finds the plural occurrence in location's name field" do + get api_search_index_url(keyword: 'services', subdomain: ENV['API_SUBDOMAIN']) + expect(json.first['name']).to eq('VRS Services') + end + + it "finds the plural occurrence in location's description field" do + get api_search_index_url(keyword: 'jobs', subdomain: ENV['API_SUBDOMAIN']) + expect(json.first['description']).to eq('Provides jobs training') + end end - it 'includes an error description' do - expect(json['description']).to eq 'lat_lng must be a comma-delimited lat,long pair of floats.' + context 'with singular version of keyword' do + it "finds the plural occurrence in location's name field" do + get api_search_index_url(keyword: 'service', subdomain: ENV['API_SUBDOMAIN']) + expect(json.first['name']).to eq('VRS Services') + end + + it "finds the plural occurrence in location's description field" do + get api_search_index_url(keyword: 'job', subdomain: ENV['API_SUBDOMAIN']) + expect(json.first['description']).to eq('Provides jobs training') + end end end - context 'when keyword only matches one location' do - it 'only returns 1 result' do + describe 'specs that depend on :location and :nearby_loc' do + before(:all) do create(:location) create(:nearby_loc) - get api_search_index_url(keyword: 'library', subdomain: ENV['API_SUBDOMAIN']) - expect(json.length).to eq(1) end - end - context "when keyword doesn't match anything" do - it 'returns no results' do - create(:location) - create(:nearby_loc) - get api_search_index_url(keyword: 'blahab', subdomain: ENV['API_SUBDOMAIN']) - expect(json.length).to eq(0) + after(:all) do + Organization.find_each(&:destroy) + end + + context 'when keyword only matches one location' do + it 'only returns 1 result' do + get api_search_index_url(keyword: 'library', subdomain: ENV['API_SUBDOMAIN']) + expect(json.length).to eq(1) + end + end + + context "when keyword doesn't match anything" do + it 'returns no results' do + get api_search_index_url(keyword: 'blahab', subdomain: ENV['API_SUBDOMAIN']) + expect(json.length).to eq(0) + end + end + + context 'with language parameter' do + it 'finds organizations that match the language' do + get api_search_index_url(keyword: 'library', language: 'arabic', subdomain: ENV['API_SUBDOMAIN']) + expect(json.first['name']).to eq('Library') + end + end + + context 'with keyword and location parameters' do + it 'only returns locations matching both parameters' do + get api_search_index_url(keyword: 'books', location: 'Burlingame', subdomain: ENV['API_SUBDOMAIN']) + expect(headers['X-Total-Count']).to eq '1' + expect(json.first['name']).to eq('Library') + end + end + + context 'when keyword parameter has multiple words' do + it 'only returns locations matching all words' do + get api_search_index_url(keyword: 'library books jobs', subdomain: ENV['API_SUBDOMAIN']) + expect(headers['X-Total-Count']).to eq '1' + expect(json.first['name']).to eq('Library') + end end end @@ -164,28 +237,7 @@ end end - context 'with language parameter' do - it 'finds organizations that match the language' do - create(:location) - create(:nearby_loc) - get api_search_index_url(keyword: 'library', language: 'arabic', subdomain: ENV['API_SUBDOMAIN']) - expect(json.first['name']).to eq('Library') - end - end - context 'with singular version of keyword' do - it "finds the plural occurrence in location's name field" do - create(:location) - get api_search_index_url(keyword: 'service', subdomain: ENV['API_SUBDOMAIN']) - expect(json.first['name']).to eq('VRS Services') - end - - it "finds the plural occurrence in location's description field" do - create(:location) - get api_search_index_url(keyword: 'job', subdomain: ENV['API_SUBDOMAIN']) - expect(json.first['description']).to eq('Provides jobs training') - end - it 'finds the plural occurrence in organization name field' do create(:nearby_loc) get api_search_index_url(keyword: 'food stamp', subdomain: ENV['API_SUBDOMAIN']) @@ -200,18 +252,6 @@ end context 'with plural version of keyword' do - it "finds the plural occurrence in location's name field" do - create(:location) - get api_search_index_url(keyword: 'services', subdomain: ENV['API_SUBDOMAIN']) - expect(json.first['name']).to eq('VRS Services') - end - - it "finds the plural occurrence in location's description field" do - create(:location) - get api_search_index_url(keyword: 'jobs', subdomain: ENV['API_SUBDOMAIN']) - expect(json.first['description']).to eq('Provides jobs training') - end - it 'finds the plural occurrence in organization name field' do create(:nearby_loc) get api_search_index_url(keyword: 'food stamps', subdomain: ENV['API_SUBDOMAIN']) @@ -304,30 +344,6 @@ end end - context 'with keyword and location parameters' do - before(:each) do - create(:nearby_loc) - create(:location) - end - it 'only returns locations matching both parameters' do - get api_search_index_url(keyword: 'books', location: 'Burlingame', subdomain: ENV['API_SUBDOMAIN']) - expect(headers['X-Total-Count']).to eq '1' - expect(json.first['name']).to eq('Library') - end - end - - context 'when keyword parameter has multiple words' do - before(:each) do - create(:nearby_loc) - create(:location) - end - it 'only returns locations matching all words' do - get api_search_index_url(keyword: 'library books jobs', subdomain: ENV['API_SUBDOMAIN']) - expect(headers['X-Total-Count']).to eq '1' - expect(json.first['name']).to eq('Library') - end - end - context 'with domain parameter' do it "finds domain name when url contains 'www'" do create(:location, urls: ['http://www.smchsa.org']) diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 15300d2b1..f069d3bf4 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -27,39 +27,25 @@ config.include DefaultHeaders, type: :request config.before(:suite) do - DatabaseCleaner.strategy = :transaction DatabaseCleaner.clean_with(:truncation) end - config.around(:each) do |example| - DatabaseCleaner.cleaning do - example.run - end + config.before(:each) do + DatabaseCleaner.strategy = :transaction end config.before(:each) do + DatabaseCleaner.start Bullet.start_request if Bullet.enable? end config.after(:each) do + DatabaseCleaner.clean Bullet.end_request if Bullet.enable? end - # config.before(:suite) do + # config.after(:all) do # DatabaseCleaner.clean_with(:truncation) - # end - - # config.before(:each) do - # DatabaseCleaner.strategy = :transaction - # end - - # config.before(:each) do - # DatabaseCleaner.start - # Bullet.start_request if Bullet.enable? - # end - - # config.after(:each) do - # DatabaseCleaner.clean # Bullet.end_request if Bullet.enable? # end diff --git a/spec/support/api/model_helpers.rb b/spec/support/api/model_helpers.rb index 55ba070e6..4da6861f8 100644 --- a/spec/support/api/model_helpers.rb +++ b/spec/support/api/model_helpers.rb @@ -1,5 +1,4 @@ def create_service @location = create(:location) @service = @location.services.create!(attributes_for(:service)) - @location.reload end From 7701c8ca22571cff72c02f4219eef631e2467690 Mon Sep 17 00:00:00 2001 From: Moncef Belyamani Date: Mon, 30 Jun 2014 14:51:18 -0400 Subject: [PATCH 03/17] Add locations index view & logic for super admin vs. regular admin. - Redirect admins to locations index view after signing in - Admin must be signed in to access locations index view - Add 'Your locations' link to nav bar - Add super_admin boolean column to Admins table and set to false by default - Update INSTALL.md with instructions for setting an admin as a super admin - Create admins to test with in db/seeds.rb --- INSTALL.md | 16 +- app/controllers/admin/locations_controller.rb | 109 ++++++++++++++ app/controllers/application_controller.rb | 2 +- app/views/admin/locations/index.html.haml | 23 +++ app/views/admins/shared/_navigation.html.haml | 2 +- config/routes.rb | 2 + ...0140630171418_add_super_admin_to_admins.rb | 5 + db/seeds.rb | 21 +++ db/structure.sql | 5 +- spec/factories/admins.rb | 17 ++- spec/factories/locations.rb | 5 + spec/features/admin/dashboard_spec.rb | 14 +- spec/features/admin/sign_in_spec.rb | 27 ++-- spec/features/admin/visit_locations_spec.rb | 140 ++++++++++++++++++ spec/support/features/session_helpers.rb | 14 ++ 15 files changed, 375 insertions(+), 27 deletions(-) create mode 100644 app/controllers/admin/locations_controller.rb create mode 100644 app/views/admin/locations/index.html.haml create mode 100644 db/migrate/20140630171418_add_super_admin_to_admins.rb create mode 100644 spec/features/admin/visit_locations_spec.rb diff --git a/INSTALL.md b/INSTALL.md index 8a81b1a4a..3ef32e474 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -75,9 +75,9 @@ local [Admin Interface][admin] if it's pointing to your local API. ### Verify the app is returning JSON -[http://localhost:8080/api/locations](http://localhost:8080/api/locations) +[http://lvh.me:8080/api/locations](http://lvh.me:8080/api/locations) -[http://localhost:8080/api/search?keyword=food](http://localhost:8080/api/search?keyword=food) +[http://lvh.me:8080/api/search?keyword=food](http://lvh.me:8080/api/search?keyword=food) We recommend the [JSONView][jsonview] Google Chrome extension for formatting the JSON response so it is easier to read in the browser. @@ -151,11 +151,17 @@ To reset your local database and populate it again with your clean data: script/import ``` -### User authentication (for the developer portal) +### User and Admin authentication (for the developer portal and admin interface) -The app automatically sets up users you can [sign in][sign_in] with. +The app automatically sets up users and admins you can sign in with. Their username and password are stored in [db/seeds.rb][seeds]. -[sign_in]: http://localhost:8080/users/sign_in [seeds]: https://github.com/codeforamerica/ohana-api/blob/master/db/seeds.rb +To set an admin as a Super Admin: + + psql ohana_api_development + UPDATE "admins" SET super_admin = true WHERE id = 3; + \q + +To access the admin interface, visit [http://lvh.me:8080/admin/](http://lvh.me:8080/admin/). diff --git a/app/controllers/admin/locations_controller.rb b/app/controllers/admin/locations_controller.rb new file mode 100644 index 000000000..77c122457 --- /dev/null +++ b/app/controllers/admin/locations_controller.rb @@ -0,0 +1,109 @@ +class Admin + class LocationsController < ApplicationController + before_action :authenticate_admin! + layout 'admin' + + def index + if current_admin.super_admin? + @locations = Location.page(params[:page]).per(params[:per_page]). + order('created_at DESC') + else + @locations = perform_search + @org = @locations.includes(:organization).first.organization if @locations.present? + end + end + + def new + @location = Location.new + end + + def edit + @location = Location.find(params[:id]) + end + + def update + @location = Location.find(params[:id]) + + respond_to do |format| + if @location.update(params[:location]) + format.html do + redirect_to @location, + notice: 'Location was successfully updated.' + end + else + format.html { render :edit } + end + end + end + + def create + @location = Location.new(params[:location]) + + respond_to do |format| + if @location.save + format.html do + redirect_to @location, + notice: 'Location was successfully created.' + end + else + format.html { render :new } + end + end + end + + def destroy + location = Location.find(params[:id]) + location.destroy + respond_to do |format| + format.html { redirect_to admin_locations_path } + end + end + + private + + def domain + current_admin.email.split('@').last + end + + def user_allowed_access_to_location?(location) + if current_admin_has_generic_email? + emails_match_user_email?(location) || admins_match_user_email?(location) + else + emails_match_domain?(location) || urls_match_domain?(location) + end + end + + def urls_match_domain?(location) + return false unless location[:urls].present? + location.urls.select { |url| url.include?(domain) }.length > 0 + end + + def emails_match_domain?(location) + return false unless location[:emails].present? + location.emails.select { |email| email.include?(domain) }.length > 0 + end + + def emails_match_user_email?(location) + return false unless location[:emails].present? + location.emails.select { |email| email == current_admin.email }.length > 0 + end + + def admins_match_user_email?(location) + return false unless location[:admin_emails].present? + location.admin_emails.select { |email| email == current_admin.email }.length > 0 + end + + def current_admin_has_generic_email? + generic_domains = SETTINGS[:generic_domains] + generic_domains.include?(domain) + end + + def perform_search + if current_admin_has_generic_email? + Location.text_search(email: current_admin.email) + else + Location.text_search(domain: domain) + end + end + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 007318abe..cee44ba89 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -26,7 +26,7 @@ class ApplicationController < ActionController::Base def after_sign_in_path_for(resource) return root_url if resource.is_a?(User) - return admin_dashboard_path if resource.is_a?(Admin) + return admin_locations_path if resource.is_a?(Admin) end def after_sign_out_path_for(resource) diff --git a/app/views/admin/locations/index.html.haml b/app/views/admin/locations/index.html.haml new file mode 100644 index 000000000..eabe46a5b --- /dev/null +++ b/app/views/admin/locations/index.html.haml @@ -0,0 +1,23 @@ +%p + Welcome back, #{current_admin.name}! +%p +Below you should see a list of locations that you are allowed to administer based on your email address. +If there are any locations missing, please #{mail_to "sanmateoco@codeforamerica.org", "let us know"}. +%p +%p + To start updating, click on one of the links, which will take you to the details page + for the location. +- if current_admin.super_admin? + %p As a super admin, you have access to all locations in the database. Please make updates responsibly. +%br +- if !current_admin.super_admin? && @org.present? + %p + %strong="#{@org.name} locations:" +%p + - @locations.each do |location| + %ul + = link_to location.name, edit_admin_location_path(location) + +- if !current_admin.super_admin? && @org.present? + %p + = link_to "Add a new location", new_admin_location_path, :class => "btn btn-primary" diff --git a/app/views/admins/shared/_navigation.html.haml b/app/views/admins/shared/_navigation.html.haml index 69b026323..a09c12f0e 100644 --- a/app/views/admins/shared/_navigation.html.haml +++ b/app/views/admins/shared/_navigation.html.haml @@ -7,7 +7,7 @@ %li = link_to 'Edit account', edit_admin_registration_path %li - /= link_to 'Your locations', admin_locations_path + = link_to 'Your locations', admin_locations_path %li = link_to 'Sign out', destroy_admin_session_path, method: 'delete' - else diff --git a/config/routes.rb b/config/routes.rb index e3ebf0330..77d26c6c2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -12,6 +12,8 @@ namespace :admin do root to: 'dashboard#index', as: :dashboard + resources :locations, except: :show + get 'locations/:id', to: 'admin/locations#edit' end devise_for :admins, path: 'admin', controllers: { registrations: 'admin/registrations' } diff --git a/db/migrate/20140630171418_add_super_admin_to_admins.rb b/db/migrate/20140630171418_add_super_admin_to_admins.rb new file mode 100644 index 000000000..7550daaa6 --- /dev/null +++ b/db/migrate/20140630171418_add_super_admin_to_admins.rb @@ -0,0 +1,5 @@ +class AddSuperAdminToAdmins < ActiveRecord::Migration + def change + add_column :admins, :super_admin, :boolean, default: false + end +end diff --git a/db/seeds.rb b/db/seeds.rb index d9f803719..aefdfee34 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -20,3 +20,24 @@ password: 'mong01dtest', password_confirmation: 'mong01dtest' user2.confirm! + +puts '===> Setting up first test admin...' +admin = Admin.create! :name => 'admin with custom domain name', + :email => 'ohana@samaritanhouse.com', + :password => 'ohanatest', + :password_confirmation => 'ohanatest' +admin.confirm! + +puts '===> Setting up second test admin...' +admin2 = Admin.create! :name => 'admin with generic email', + :email => 'ohana@gmail.com', + :password => 'ohanatest', + :password_confirmation => 'ohanatest' +admin2.confirm! + +puts '===> Setting up test super admin...' +admin3 = Admin.create! :name => 'Super Admin', + :email => 'masteradmin@ohanapi.org', + :password => 'ohanatest', + :password_confirmation => 'ohanatest' +admin3.confirm! diff --git a/db/structure.sql b/db/structure.sql index f797a70a9..28ca9306f 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -126,7 +126,8 @@ CREATE TABLE admins ( confirmation_sent_at timestamp without time zone, unconfirmed_email character varying(255), created_at timestamp without time zone, - updated_at timestamp without time zone + updated_at timestamp without time zone, + super_admin boolean DEFAULT false ); @@ -1074,3 +1075,5 @@ INSERT INTO schema_migrations (version) VALUES ('20140522153640'); INSERT INTO schema_migrations (version) VALUES ('20140629181523'); +INSERT INTO schema_migrations (version) VALUES ('20140630171418'); + diff --git a/spec/factories/admins.rb b/spec/factories/admins.rb index 8ea47ee26..56227e195 100644 --- a/spec/factories/admins.rb +++ b/spec/factories/admins.rb @@ -1,11 +1,22 @@ # Read about factories at https://github.com/thoughtbot/factory_girl - FactoryGirl.define do factory :admin do - name 'Test Admin' - email 'admin@example.com' + name 'Org Admin' + email 'moncef@samaritanhouse.com' password 'ohanatest' password_confirmation 'ohanatest' confirmed_at Time.now + + factory :super_admin do + name 'Super Admin' + super_admin true + end + end + + factory :unconfirmed_admin, class: :admin do + name 'Unconfirmed admin' + email 'invalid@example.com' + password 'ohanatest' + password_confirmation 'ohanatest' end end diff --git a/spec/factories/locations.rb b/spec/factories/locations.rb index 4727dcbff..9386d4263 100644 --- a/spec/factories/locations.rb +++ b/spec/factories/locations.rb @@ -15,6 +15,11 @@ admin_emails ['moncef@smcgov.org'] end + factory :location_for_org_admin do + name 'Samaritan House' + urls ['http://samaritanhouse.com'] + end + factory :loc_with_extra_whitespace do description ' Provides job training' hours ' Monday-Friday 10am-3pm ' diff --git a/spec/features/admin/dashboard_spec.rb b/spec/features/admin/dashboard_spec.rb index f8b5dc4f1..0e3b47603 100644 --- a/spec/features/admin/dashboard_spec.rb +++ b/spec/features/admin/dashboard_spec.rb @@ -45,6 +45,12 @@ expect(page).not_to have_link 'Home', href: root_path end end + + it 'does not include a link to Your locations in the navigation' do + within '.navbar' do + expect(page).not_to have_link 'Your locations', href: admin_locations_path + end + end end context 'when signed in' do @@ -59,7 +65,7 @@ end it 'greets the admin by their name' do - expect(page).to have_content 'Welcome back, Test Admin!' + expect(page).to have_content 'Welcome back, Org Admin!' end it 'does not include a link to the sign up page in the navigation' do @@ -93,5 +99,11 @@ expect(page).to have_content "Logged in as #{@admin.name}" end end + + it 'includes a link to Your locations in the navigation' do + within '.navbar' do + expect(page).to have_link 'Your locations', href: admin_locations_path + end + end end end diff --git a/spec/features/admin/sign_in_spec.rb b/spec/features/admin/sign_in_spec.rb index c6abe2830..c4123afcb 100644 --- a/spec/features/admin/sign_in_spec.rb +++ b/spec/features/admin/sign_in_spec.rb @@ -34,25 +34,22 @@ context 'with correct credentials' do before :each do valid_admin = create(:admin) - visit '/admin/sign_in' - within('#new_admin') do - fill_in 'admin_email', with: valid_admin.email - fill_in 'admin_password', with: valid_admin.password - end - click_button 'Sign in' + sign_in_admin(valid_admin.email, valid_admin.password) end - it 'sets the current path to the admin root page' do - expect(current_path).to eq(admin_dashboard_path) + it 'sets the current path to the admin locations path' do + expect(current_path).to eq(admin_locations_path) end - xit "displays the admin's locations" do + it "displays the admin's locations" do + create(:location_for_org_admin) + visit '/admin/locations' expect(page).to have_content 'Below you should see a list' - expect(page).to have_content 'Samaritan House locations' + expect(page).to have_content 'Parent Agency locations' end it 'greets the admin by their name' do - expect(page).to have_content 'Welcome back, Test Admin!' + expect(page).to have_content 'Welcome back, Org Admin!' end it 'displays a success message' do @@ -61,13 +58,13 @@ end scenario 'with invalid credentials' do - sign_in('hello@example.com', 'wrongpassword') + sign_in_admin('hello@example.com', 'wrongpassword') expect(page).to have_content 'Invalid email or password' end - scenario 'with an unconfirmed user' do - unconfirmed_user = create(:unconfirmed_user) - sign_in(unconfirmed_user.email, unconfirmed_user.password) + scenario 'with an unconfirmed admin' do + unconfirmed_admin = create(:unconfirmed_admin) + sign_in_admin(unconfirmed_admin.email, unconfirmed_admin.password) expect(page) .to have_content 'You have to confirm your account before continuing.' end diff --git a/spec/features/admin/visit_locations_spec.rb b/spec/features/admin/visit_locations_spec.rb new file mode 100644 index 000000000..13d870133 --- /dev/null +++ b/spec/features/admin/visit_locations_spec.rb @@ -0,0 +1,140 @@ +require 'rails_helper' + +feature 'Locations page' do + include Warden::Test::Helpers + + context 'when not signed in' do + before :each do + visit '/admin/locations' + end + + it 'redirects to the admin sign in page' do + expect(current_path).to eq(new_admin_session_path) + end + + it 'prompts the user to sign in or sign up' do + expect(page). + to have_content 'You need to sign in or sign up before continuing.' + end + + it 'includes a link to the sign in page in the navigation' do + within '.navbar' do + expect(page).to have_link 'Sign in', href: new_admin_session_path + end + end + + it 'includes a link to the sign up page in the navigation' do + within '.navbar' do + expect(page).to have_link 'Sign up', href: new_admin_registration_path + end + end + + it 'does not include a link to the Home page in the navigation' do + within '.navbar' do + expect(page).not_to have_link 'Home', href: root_path + end + end + + it 'does not include a link to Your locations in the navigation' do + within '.navbar' do + expect(page).not_to have_link 'Your locations', href: admin_locations_path + end + end + end + + context 'when signed in' do + before :each do + Warden.test_mode! + login_admin + visit '/admin/locations' + end + + after :each do + Warden.test_reset! + end + + it 'displays instructions for editing locations' do + expect(page).to have_content 'Below you should see a list of locations' + expect(page).to have_content 'To start updating, click on one of the links' + end + + it 'only shows links that belong to the admin' do + create(:location) + create(:location_for_org_admin) + visit '/admin/locations' + expect(page).not_to have_link 'VRS Services' + expect(page).to have_link 'Samaritan House' + end + + it 'greets the admin by their name' do + expect(page).to have_content 'Welcome back, Org Admin!' + end + + it 'does not include a link to the sign up page in the navigation' do + within '.navbar' do + expect(page).not_to have_link 'Sign up' + end + end + + it 'does not include a link to the sign in page in the navigation' do + within '.navbar' do + expect(page).not_to have_link 'Sign in' + end + end + + it 'includes a link to sign out in the navigation' do + within '.navbar' do + expect(page). + to have_link 'Sign out', href: destroy_admin_session_path + end + end + + it 'includes a link to the Edit Account page in the navigation' do + within '.navbar' do + expect(page). + to have_link 'Edit account', href: edit_admin_registration_path + end + end + + it 'displays the name of the logged in admin in the navigation' do + within '.navbar' do + expect(page).to have_content "Logged in as #{@admin.name}" + end + end + + it 'includes a link to Your locations in the navigation' do + within '.navbar' do + expect(page).to have_link 'Your locations', href: admin_locations_path + end + end + end + + context 'when signed in as super admin' do + before :each do + Warden.test_mode! + login_super_admin + visit '/admin/locations' + end + + after :each do + Warden.test_reset! + end + + it 'displays instructions for editing locations' do + expect(page).to have_content 'As a super admin' + end + + it 'shows all locations' do + create(:location) + create(:location_for_org_admin) + visit '/admin/locations' + expect(page).to have_link 'VRS Services' + expect(page).to have_link 'Samaritan House' + expect(page).not_to have_content 'Parent Agency locations' + end + + it 'greets the admin by their name' do + expect(page).to have_content 'Welcome back, Super Admin!' + end + end +end diff --git a/spec/support/features/session_helpers.rb b/spec/support/features/session_helpers.rb index 386495a15..721d239f3 100644 --- a/spec/support/features/session_helpers.rb +++ b/spec/support/features/session_helpers.rb @@ -7,6 +7,11 @@ def login_admin login_as(@admin, scope: :admin) end + def login_super_admin + @super_admin = FactoryGirl.create(:super_admin) + login_as(@super_admin, scope: :admin) + end + def login_user user = FactoryGirl.create(:user) login_as(user, scope: :user) @@ -21,6 +26,15 @@ def sign_in(email, password) click_button 'Sign in' end + def sign_in_admin(email, password) + visit '/admin/sign_in' + within('#new_admin') do + fill_in 'admin_email', with: email + fill_in 'admin_password', with: password + end + click_button 'Sign in' + end + def sign_up(name, email, password, confirmation) visit '/users/sign_up' fill_in 'user_name', with: name From a9bd7a1e5168b79f58623424f167e129c298ec3c Mon Sep 17 00:00:00 2001 From: Moncef Belyamani Date: Thu, 3 Jul 2014 22:43:31 -0400 Subject: [PATCH 04/17] Allow admins to update locations This commit includes the following features from the standalone interface: - ability to delete a location, with modal window warning - ability to dynamically add fields via JS for contacts, faxes, phones, address, mail address, websites, emails, and admin emails - CSS styling for edit location form - admin and super admin authorization to control who gets to see which locations Other changes: - move custom JSON errors when rescuing from Rails errors to a controller concern within the API namespace so that they don't get triggered within the admin namespace. --- .rubocop.yml | 3 + Gemfile | 1 + Gemfile.lock | 8 + app/assets/javascripts/application.js | 1 + app/assets/javascripts/form.js.coffee | 22 +++ app/assets/stylesheets/application.css.scss | 149 +++++++++++++++++- app/controllers/admin/locations_controller.rb | 76 ++++----- app/controllers/api/v1/address_controller.rb | 1 + app/controllers/api/v1/contacts_controller.rb | 1 + app/controllers/api/v1/faxes_controller.rb | 1 + .../api/v1/locations_controller.rb | 1 + .../api/v1/mail_address_controller.rb | 1 + .../api/v1/organizations_controller.rb | 1 + app/controllers/api/v1/phones_controller.rb | 1 + app/controllers/api/v1/search_controller.rb | 1 + app/controllers/api/v1/services_controller.rb | 1 + app/controllers/application_controller.rb | 47 ------ app/controllers/concerns/custom_errors.rb | 58 +++++++ app/decorators/admin/admin_decorator.rb | 55 +++++++ app/helpers/admin/form_helper.rb | 18 +++ app/models/admin.rb | 6 + app/models/location.rb | 22 ++- .../_confirm_delete_location.html.haml | 19 +++ app/views/admin/locations/_form.html.haml | 40 +++++ .../locations/confirm_delete_location.js.erb | 1 + app/views/admin/locations/edit.html.haml | 5 + .../locations/forms/_accessibility.html.haml | 9 ++ .../admin/locations/forms/_address.html.haml | 12 ++ .../locations/forms/_address_fields.html.haml | 13 ++ .../forms/_admin_email_fields.html.haml | 2 + .../locations/forms/_admin_emails.html.haml | 15 ++ .../locations/forms/_contact_fields.html.haml | 20 +++ .../admin/locations/forms/_contacts.html.haml | 12 ++ .../locations/forms/_description.html.haml | 7 + .../locations/forms/_email_fields.html.haml | 2 + .../admin/locations/forms/_emails.html.haml | 15 ++ .../locations/forms/_fax_fields.html.haml | 8 + .../admin/locations/forms/_faxes.html.haml | 15 ++ .../admin/locations/forms/_hours.html.haml | 22 +++ .../locations/forms/_location_name.html.haml | 7 + .../locations/forms/_mail_address.html.haml | 10 ++ .../forms/_mail_address_fields.html.haml | 3 + .../forms/_new_location_form.html.haml | 23 +++ .../locations/forms/_phone_fields.html.haml | 14 ++ .../admin/locations/forms/_phones.html.haml | 15 ++ .../locations/forms/_short_desc.html.haml | 7 + .../locations/forms/_text_hours.html.haml | 9 ++ .../locations/forms/_transportation.html.haml | 7 + .../locations/forms/_url_fields.html.haml | 2 + .../admin/locations/forms/_urls.html.haml | 19 +++ app/views/admin/locations/new.html.haml | 5 + app/views/shared/_messages.html.haml | 4 +- config/routes.rb | 3 +- spec/api/patch_location_spec.rb | 3 +- spec/factories/admins.rb | 8 + spec/features/admin/dashboard_spec.rb | 7 - .../locations/update_accessibility_spec.rb | 54 +++++++ .../admin/locations/update_address_spec.rb | 80 ++++++++++ .../admin/locations/update_contacts_spec.rb | 142 +++++++++++++++++ .../admin/locations/visit_location_spec.rb | 72 +++++++++ .../{ => locations}/visit_locations_spec.rb | 12 -- spec/features/admin/sign_out_spec.rb | 7 - .../create_new_api_application_spec.rb | 6 - spec/features/homepage_text_spec.rb | 6 - spec/features/sign_out_spec.rb | 7 - spec/features/update_api_application_spec.rb | 6 - spec/rails_helper.rb | 36 +---- spec/support/bullet.rb | 10 ++ spec/support/database_cleaner.rb | 22 +++ spec/support/features/form_helpers.rb | 66 ++++++++ spec/support/features/session_helpers.rb | 4 + spec/support/warden.rb | 11 ++ 72 files changed, 1190 insertions(+), 189 deletions(-) create mode 100644 app/assets/javascripts/form.js.coffee create mode 100644 app/controllers/concerns/custom_errors.rb create mode 100644 app/decorators/admin/admin_decorator.rb create mode 100644 app/helpers/admin/form_helper.rb create mode 100644 app/views/admin/locations/_confirm_delete_location.html.haml create mode 100644 app/views/admin/locations/_form.html.haml create mode 100644 app/views/admin/locations/confirm_delete_location.js.erb create mode 100644 app/views/admin/locations/edit.html.haml create mode 100644 app/views/admin/locations/forms/_accessibility.html.haml create mode 100644 app/views/admin/locations/forms/_address.html.haml create mode 100644 app/views/admin/locations/forms/_address_fields.html.haml create mode 100644 app/views/admin/locations/forms/_admin_email_fields.html.haml create mode 100644 app/views/admin/locations/forms/_admin_emails.html.haml create mode 100644 app/views/admin/locations/forms/_contact_fields.html.haml create mode 100644 app/views/admin/locations/forms/_contacts.html.haml create mode 100644 app/views/admin/locations/forms/_description.html.haml create mode 100644 app/views/admin/locations/forms/_email_fields.html.haml create mode 100644 app/views/admin/locations/forms/_emails.html.haml create mode 100644 app/views/admin/locations/forms/_fax_fields.html.haml create mode 100644 app/views/admin/locations/forms/_faxes.html.haml create mode 100644 app/views/admin/locations/forms/_hours.html.haml create mode 100644 app/views/admin/locations/forms/_location_name.html.haml create mode 100644 app/views/admin/locations/forms/_mail_address.html.haml create mode 100644 app/views/admin/locations/forms/_mail_address_fields.html.haml create mode 100644 app/views/admin/locations/forms/_new_location_form.html.haml create mode 100644 app/views/admin/locations/forms/_phone_fields.html.haml create mode 100644 app/views/admin/locations/forms/_phones.html.haml create mode 100644 app/views/admin/locations/forms/_short_desc.html.haml create mode 100644 app/views/admin/locations/forms/_text_hours.html.haml create mode 100644 app/views/admin/locations/forms/_transportation.html.haml create mode 100644 app/views/admin/locations/forms/_url_fields.html.haml create mode 100644 app/views/admin/locations/forms/_urls.html.haml create mode 100644 app/views/admin/locations/new.html.haml create mode 100644 spec/features/admin/locations/update_accessibility_spec.rb create mode 100644 spec/features/admin/locations/update_address_spec.rb create mode 100644 spec/features/admin/locations/update_contacts_spec.rb create mode 100644 spec/features/admin/locations/visit_location_spec.rb rename spec/features/admin/{ => locations}/visit_locations_spec.rb (95%) create mode 100644 spec/support/bullet.rb create mode 100644 spec/support/database_cleaner.rb create mode 100644 spec/support/features/form_helpers.rb create mode 100644 spec/support/warden.rb diff --git a/.rubocop.yml b/.rubocop.yml index e797dff1c..bd1391c6e 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -28,5 +28,8 @@ Style/ClassLength: CountComments: false Max: 100 +Style/GuardClause: + MinBodyLength: 3 + Rails/HasAndBelongsToMany: Enabled: false \ No newline at end of file diff --git a/Gemfile b/Gemfile index ada502bb0..075429879 100644 --- a/Gemfile +++ b/Gemfile @@ -67,6 +67,7 @@ end group :test do gem 'database_cleaner', '>= 1.0.0.RC1' gem 'capybara' + gem 'poltergeist' gem 'shoulda-matchers', require: false gem 'coveralls', require: false gem 'rubocop' diff --git a/Gemfile.lock b/Gemfile.lock index c5f5a4a75..0e7438697 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -53,6 +53,7 @@ GEM rack (>= 1.0.0) rack-test (>= 0.5.4) xpath (~> 2.0) + cliver (0.3.2) coderay (1.1.0) coffee-rails (4.0.1) coffee-script (>= 2.2.0) @@ -137,6 +138,11 @@ GEM activerecord (>= 3.1) activesupport (>= 3.1) arel + poltergeist (1.5.1) + capybara (~> 2.1) + cliver (~> 0.3.1) + multi_json (~> 1.0) + websocket-driver (>= 0.2.0) polyglot (0.3.5) powerpack (0.0.9) protected_attributes (1.0.8) @@ -243,6 +249,7 @@ GEM uniform_notifier (1.6.2) warden (1.2.3) rack (>= 1.0) + websocket-driver (0.3.3) xpath (2.0.0) nokogiri (~> 1.3) @@ -275,6 +282,7 @@ DEPENDENCIES passenger pg pg_search + poltergeist protected_attributes quiet_assets (>= 1.0.2) rack-cors diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 921aebe22..b40fe089f 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -13,4 +13,5 @@ //= require jquery //= require jquery_ujs //= require bootstrap +//= require bootstrap-modal //= require_tree . diff --git a/app/assets/javascripts/form.js.coffee b/app/assets/javascripts/form.js.coffee new file mode 100644 index 000000000..4f9831e23 --- /dev/null +++ b/app/assets/javascripts/form.js.coffee @@ -0,0 +1,22 @@ +jQuery -> + $('.edit_location').on 'click', '.delete_association', (event) -> + $(this).prevAll('input[type=hidden]').val('1') + $(this).closest('fieldset').hide() + event.preventDefault() + + $('.edit_location').on 'click', '.delete_attribute', (event) -> + $(this).closest('fieldset').find("input").val('') + $(this).closest('fieldset').hide() + event.preventDefault() + + $('.edit_location').on 'click', '.add_fields', (event) -> + time = new Date().getTime() + regexp = new RegExp($(this).data('id'), 'g') + $(this).before($(this).data('fields').replace(regexp, time)) + event.preventDefault() + + $('.new_location').on 'click', '.add_fields', (event) -> + time = new Date().getTime() + regexp = new RegExp($(this).data('id'), 'g') + $(this).before($(this).data('fields').replace(regexp, time)) + event.preventDefault() \ No newline at end of file diff --git a/app/assets/stylesheets/application.css.scss b/app/assets/stylesheets/application.css.scss index c2e96256c..a2bf6ecee 100644 --- a/app/assets/stylesheets/application.css.scss +++ b/app/assets/stylesheets/application.css.scss @@ -15,15 +15,14 @@ */ .content { - background-color: #eee; + background-color: #fff; padding: 20px; margin: 0 -20px; /* negative indent the amount of the padding to maintain the grid system */ -webkit-border-radius: 0 0 6px 6px; -moz-border-radius: 0 0 6px 6px; border-radius: 0 0 6px 6px; - -webkit-box-shadow: 0 1px 2px rgba(0,0,0,.15); - -moz-box-shadow: 0 1px 2px rgba(0,0,0,.15); - box-shadow: 0 1px 2px rgba(0,0,0,.15); + font-size: 16px; + line-height: 1.428; } // creates styles for text in the navbar that is not a link @@ -33,3 +32,145 @@ color:#DE9292; display:block; } + +.content-box +{ + -webkit-box-shadow: 0 1px 2px rgba(0,0,0,.15); + -moz-box-shadow: 0 1px 2px rgba(0,0,0,.15); + box-shadow: 0 1px 2px rgba(0,0,0,.15); + + border:1px solid #DE9292; + margin-bottom:10px; + padding:20px; + + h1 + { + color:#DE9292; + font-size:14px; + margin:0; + padding:0; + text-transform: uppercase; + } +} + +.inst-box +{ + -webkit-box-shadow: 0 1px 2px rgba(0,0,0,.15); + -moz-box-shadow: 0 1px 2px rgba(0,0,0,.15); + box-shadow: 0 1px 2px rgba(0,0,0,.15); + + border:1px solid #C8D4E8; + margin-bottom:20px; + header + { + background:#cbe0f3; + margin-bottom:10px; + padding:10px; + + .desc + { + font-size:14px; + display:inline; + + em + { + color:#b5121b; + } + } + label {font-weight: bold; font-size:16px;} + } + input + { + margin:0px 10px 20px 0px; + } + + p + { + padding:10px; + } + fieldset + { + padding:10px; + } +} + +#categories, #accessibility +{ + ul {list-style-type: none; margin-left:15px;} + li {vertical-align:middle;} + input {height:20px;width:20px;margin-right:5px;margin-bottom:0px;display:inline-block;} + label {display:inline-block;vertical-align:middle;width:220px;margin-top:7px;} +} + +// margin added to accommodate for floating footer +// This needs to be added to the last form element before the save button +.edit_location +{ + margin-bottom: 60px; +} + +.danger-zone +{ + border:1px solid #DE9292; + margin-bottom:20px; + color:white; + + header + { + background:#df3e3e; + margin-bottom:10px; + padding:10px; + } + + p + { + padding:10px; + color: #000000; + } + + h4 + { + font-weight: bold; + color: #000000; + padding:10px 0 0 10px; + } +} + +a.boxed-action,a.boxed-action:link,a.boxed-action:visited +{ + display:inline-block; + color: #df3e3e; + border:1px solid #C8E8CA; + padding: 10px; + font-weight: bold; + background-color: #fcfcfc; + text-decoration:none; +} + +a.boxed-action:hover, a.boxed-action:active +{ + background-color: #df3e3e; + color: #fff; +} + +.save-box +{ + position:fixed; + bottom:0; + left:0; + right:0; + text-align:center; + + input + { + margin-top:20px; + margin-bottom:20px; + } + p + { + display:inline-block; + margin-top:20px; + margin-right:20px; + margin-bottom:0px; + } +} diff --git a/app/controllers/admin/locations_controller.rb b/app/controllers/admin/locations_controller.rb index 77c122457..48a7f50a7 100644 --- a/app/controllers/admin/locations_controller.rb +++ b/app/controllers/admin/locations_controller.rb @@ -4,30 +4,47 @@ class LocationsController < ApplicationController layout 'admin' def index + @admin_decorator = AdminDecorator.new(current_admin) if current_admin.super_admin? @locations = Location.page(params[:page]).per(params[:per_page]). order('created_at DESC') else - @locations = perform_search + @locations = @admin_decorator.locations @org = @locations.includes(:organization).first.organization if @locations.present? end end def new @location = Location.new + @org = current_admin.org + if @org.present? + @location_url = @org.locations.map(&:urls).uniq.first + else + redirect_to admin_dashboard_path, + alert: "Sorry, you don't have access to that page." + end end def edit + @admin_decorator = AdminDecorator.new(current_admin) @location = Location.find(params[:id]) + @org = @location.organization + + unless @admin_decorator.allowed_to_access_location?(@location) + redirect_to admin_dashboard_path, + alert: "Sorry, you don't have access to that page." + end end def update @location = Location.find(params[:id]) + @org = @location.organization + @admin_decorator = AdminDecorator.new(current_admin) respond_to do |format| if @location.update(params[:location]) format.html do - redirect_to @location, + redirect_to [:admin, @location], notice: 'Location was successfully updated.' end else @@ -41,11 +58,13 @@ def create respond_to do |format| if @location.save + @org = @location.organization format.html do - redirect_to @location, + redirect_to admin_locations_url, notice: 'Location was successfully created.' end else + @org = current_admin.org format.html { render :new } end end @@ -59,50 +78,13 @@ def destroy end end - private - - def domain - current_admin.email.split('@').last - end - - def user_allowed_access_to_location?(location) - if current_admin_has_generic_email? - emails_match_user_email?(location) || admins_match_user_email?(location) - else - emails_match_domain?(location) || urls_match_domain?(location) - end - end - - def urls_match_domain?(location) - return false unless location[:urls].present? - location.urls.select { |url| url.include?(domain) }.length > 0 - end - - def emails_match_domain?(location) - return false unless location[:emails].present? - location.emails.select { |email| email.include?(domain) }.length > 0 - end - - def emails_match_user_email?(location) - return false unless location[:emails].present? - location.emails.select { |email| email == current_admin.email }.length > 0 - end - - def admins_match_user_email?(location) - return false unless location[:admin_emails].present? - location.admin_emails.select { |email| email == current_admin.email }.length > 0 - end - - def current_admin_has_generic_email? - generic_domains = SETTINGS[:generic_domains] - generic_domains.include?(domain) - end - - def perform_search - if current_admin_has_generic_email? - Location.text_search(email: current_admin.email) - else - Location.text_search(domain: domain) + def confirm_delete_location + @loc_name = params[:loc_name] + @org_name = params[:org_name] + @location_id = params[:location_id] + respond_to do |format| + format.html + format.js end end end diff --git a/app/controllers/api/v1/address_controller.rb b/app/controllers/api/v1/address_controller.rb index b353ab071..f9d88af6c 100644 --- a/app/controllers/api/v1/address_controller.rb +++ b/app/controllers/api/v1/address_controller.rb @@ -2,6 +2,7 @@ module Api module V1 class AddressController < ApplicationController include TokenValidator + include CustomErrors def update address = Address.find(params[:id]) diff --git a/app/controllers/api/v1/contacts_controller.rb b/app/controllers/api/v1/contacts_controller.rb index c296fb8c4..5d321a30a 100644 --- a/app/controllers/api/v1/contacts_controller.rb +++ b/app/controllers/api/v1/contacts_controller.rb @@ -2,6 +2,7 @@ module Api module V1 class ContactsController < ApplicationController include TokenValidator + include CustomErrors def index location = Location.find(params[:location_id]) diff --git a/app/controllers/api/v1/faxes_controller.rb b/app/controllers/api/v1/faxes_controller.rb index 6b4301a94..e946f2001 100644 --- a/app/controllers/api/v1/faxes_controller.rb +++ b/app/controllers/api/v1/faxes_controller.rb @@ -2,6 +2,7 @@ module Api module V1 class FaxesController < ApplicationController include TokenValidator + include CustomErrors def index location = Location.find(params[:location_id]) diff --git a/app/controllers/api/v1/locations_controller.rb b/app/controllers/api/v1/locations_controller.rb index 89759f0bc..e4216a44a 100644 --- a/app/controllers/api/v1/locations_controller.rb +++ b/app/controllers/api/v1/locations_controller.rb @@ -3,6 +3,7 @@ module V1 class LocationsController < ApplicationController include TokenValidator include PaginationHeaders + include CustomErrors def index locations = Location.includes(:organization, :address, :phones). diff --git a/app/controllers/api/v1/mail_address_controller.rb b/app/controllers/api/v1/mail_address_controller.rb index b371e425b..72c649f86 100644 --- a/app/controllers/api/v1/mail_address_controller.rb +++ b/app/controllers/api/v1/mail_address_controller.rb @@ -2,6 +2,7 @@ module Api module V1 class MailAddressController < ApplicationController include TokenValidator + include CustomErrors def update mail_address = MailAddress.find(params[:id]) diff --git a/app/controllers/api/v1/organizations_controller.rb b/app/controllers/api/v1/organizations_controller.rb index 549cba20c..c21e4e14e 100644 --- a/app/controllers/api/v1/organizations_controller.rb +++ b/app/controllers/api/v1/organizations_controller.rb @@ -3,6 +3,7 @@ module V1 class OrganizationsController < ApplicationController include TokenValidator include PaginationHeaders + include CustomErrors def index orgs = Organization.page(params[:page]).per(params[:per_page]) diff --git a/app/controllers/api/v1/phones_controller.rb b/app/controllers/api/v1/phones_controller.rb index cb170bf63..f21206846 100644 --- a/app/controllers/api/v1/phones_controller.rb +++ b/app/controllers/api/v1/phones_controller.rb @@ -2,6 +2,7 @@ module Api module V1 class PhonesController < ApplicationController include TokenValidator + include CustomErrors def index location = Location.find(params[:location_id]) diff --git a/app/controllers/api/v1/search_controller.rb b/app/controllers/api/v1/search_controller.rb index eeaacaa10..1a260068f 100644 --- a/app/controllers/api/v1/search_controller.rb +++ b/app/controllers/api/v1/search_controller.rb @@ -2,6 +2,7 @@ module Api module V1 class SearchController < ApplicationController include PaginationHeaders + include CustomErrors def index locations = Location.text_search(params).uniq.page(params[:page]). diff --git a/app/controllers/api/v1/services_controller.rb b/app/controllers/api/v1/services_controller.rb index c593af232..986aaeb19 100644 --- a/app/controllers/api/v1/services_controller.rb +++ b/app/controllers/api/v1/services_controller.rb @@ -2,6 +2,7 @@ module Api module V1 class ServicesController < ApplicationController include TokenValidator + include CustomErrors before_action :validate_token!, only: [:update, :destroy, :create, :update_categories] diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index cee44ba89..32c912f7a 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,5 +1,3 @@ -require 'exceptions' - class ApplicationController < ActionController::Base before_action :configure_permitted_parameters, if: :devise_controller? @@ -15,13 +13,7 @@ class ApplicationController < ActionController::Base rescue_from ActionView::MissingTemplate, with: :missing_template unless Rails.application.config.consider_all_requests_local - # rescue_from StandardError, :with => :render_error - rescue_from ActiveRecord::RecordNotFound, with: :render_not_found rescue_from ActionController::RoutingError, with: :render_not_found - rescue_from ActiveRecord::RecordInvalid, with: :render_invalid_record - rescue_from ActiveRecord::SerializationTypeMismatch, with: :render_invalid_type - rescue_from Exceptions::InvalidRadius, with: :render_invalid_radius - rescue_from Exceptions::InvalidLatLon, with: :render_invalid_lat_lon end def after_sign_in_path_for(resource) @@ -58,45 +50,6 @@ def render_not_found render json: hash, status: 404 end - def render_invalid_record(exception) - hash = - { - 'status' => 422, - 'message' => 'Validation failed for resource.', - 'errors' => [exception.record.errors] - } - render json: hash, status: 422 - end - - def render_invalid_type(exception) - value = exception.message.split('-- ').last - hash = - { - 'status' => 422, - 'message' => 'Validation failed for resource.', - 'error' => "Attribute was supposed to be an Array, but was a String: #{value}." - } - render json: hash, status: 422 - end - - def render_invalid_radius - message = { - status: 400, - error: 'Argument Error', - description: 'Radius must be a Float between 0.1 and 50.' - } - render json: message, status: 400 - end - - def render_invalid_lat_lon - message = { - status: 400, - error: 'Argument Error', - description: 'lat_lng must be a comma-delimited lat,long pair of floats.' - } - render json: message, status: 400 - end - protected def configure_permitted_parameters diff --git a/app/controllers/concerns/custom_errors.rb b/app/controllers/concerns/custom_errors.rb new file mode 100644 index 000000000..dfa37649c --- /dev/null +++ b/app/controllers/concerns/custom_errors.rb @@ -0,0 +1,58 @@ +require 'exceptions' + +module CustomErrors + extend ActiveSupport::Concern + + included do + include ActiveSupport::Rescuable + + unless Rails.application.config.consider_all_requests_local + rescue_from ActiveRecord::RecordNotFound, with: :render_not_found + rescue_from ActiveRecord::RecordInvalid, with: :render_invalid_record + rescue_from ActiveRecord::SerializationTypeMismatch, with: :render_invalid_type + rescue_from Exceptions::InvalidRadius, with: :render_invalid_radius + rescue_from Exceptions::InvalidLatLon, with: :render_invalid_lat_lon + end + end + + private + + def render_invalid_record(exception) + hash = + { + 'status' => 422, + 'message' => 'Validation failed for resource.', + 'errors' => [exception.record.errors] + } + render json: hash, status: 422 + end + + def render_invalid_type(exception) + value = exception.message.split('-- ').last + hash = + { + 'status' => 422, + 'message' => 'Validation failed for resource.', + 'error' => "Attribute was supposed to be an Array, but was a String: #{value}." + } + render json: hash, status: 422 + end + + def render_invalid_radius + message = { + status: 400, + error: 'Argument Error', + description: 'Radius must be a Float between 0.1 and 50.' + } + render json: message, status: 400 + end + + def render_invalid_lat_lon + message = { + status: 400, + error: 'Argument Error', + description: 'lat_lng must be a comma-delimited lat,long pair of floats.' + } + render json: message, status: 400 + end +end diff --git a/app/decorators/admin/admin_decorator.rb b/app/decorators/admin/admin_decorator.rb new file mode 100644 index 000000000..f2561e032 --- /dev/null +++ b/app/decorators/admin/admin_decorator.rb @@ -0,0 +1,55 @@ +class Admin + class AdminDecorator + attr_reader :admin + + def initialize(admin) + @admin = admin + end + + def allowed_to_access_location?(location) + return true if location_admins_match_admin_email?(location) || admin.super_admin? + if admin_has_generic_email? + location_emails_match_admin_email?(location) + else + location_emails_match_domain?(location) || location_urls_match_domain?(location) + end + end + + def domain + admin.email.split('@').last + end + + def location_urls_match_domain?(location) + return false unless location.urls.present? + location.urls.select { |url| url.include?(domain) }.length > 0 + end + + def location_emails_match_domain?(location) + return false unless location.emails.present? + location.emails.select { |email| email.include?(domain) }.length > 0 + end + + def location_emails_match_admin_email?(location) + return false unless location.emails.present? + location.emails.include?(admin.email) + end + + def location_admins_match_admin_email?(location) + return false unless location.admin_emails.present? + location.admin_emails.include?(admin.email) + end + + def admin_has_generic_email? + generic_domains = SETTINGS[:generic_domains] + generic_domains.include?(domain) + end + + def locations + if admin_has_generic_email? + Location.text_search(email: admin.email) + else + Location.text_search(domain: domain) + end + end + end +end diff --git a/app/helpers/admin/form_helper.rb b/app/helpers/admin/form_helper.rb new file mode 100644 index 000000000..18beade1f --- /dev/null +++ b/app/helpers/admin/form_helper.rb @@ -0,0 +1,18 @@ +class Admin + module FormHelper + def link_to_add_fields(name, f, association) + new_object = f.object.class.reflect_on_association(association).klass.new + id = new_object.object_id + fields = f.fields_for(association, new_object, child_index: id) do |builder| + render("admin/locations/forms/#{association.to_s.singularize}_fields", f: builder) + end + link_to(name, '#', class: 'add_fields btn btn-primary', data: { id: id, fields: fields.gsub('\n', '') }) + end + + def link_to_add_array_fields(name, field) + id = ''.object_id + fields = render("admin/locations/forms/#{field}_fields") + link_to(name, '#', class: 'add_fields btn btn-primary', data: { id: id, fields: fields.gsub('\n', '') }) + end + end +end diff --git a/app/models/admin.rb b/app/models/admin.rb index 4e9f7e67f..5e3bc5a43 100644 --- a/app/models/admin.rb +++ b/app/models/admin.rb @@ -9,4 +9,10 @@ class Admin < ActiveRecord::Base devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable, :confirmable + + def org + domain = email.split('@').last + admin_locs = Location.text_search(domain: domain) + admin_locs.includes(:organization).first.organization if admin_locs.present? + end end diff --git a/app/models/location.rb b/app/models/location.rb index 59aedf4a8..0e3cd6f63 100644 --- a/app/models/location.rb +++ b/app/models/location.rb @@ -8,24 +8,26 @@ class Location < ActiveRecord::Base :services_attributes, :organization_id belongs_to :organization + accepts_nested_attributes_for :organization has_one :address, dependent: :destroy accepts_nested_attributes_for :address, allow_destroy: true has_many :contacts, dependent: :destroy - accepts_nested_attributes_for :contacts + accepts_nested_attributes_for :contacts, + allow_destroy: true, reject_if: :all_blank has_many :faxes, dependent: :destroy - accepts_nested_attributes_for :faxes + accepts_nested_attributes_for :faxes, allow_destroy: true has_one :mail_address, dependent: :destroy accepts_nested_attributes_for :mail_address, allow_destroy: true has_many :phones, dependent: :destroy - accepts_nested_attributes_for :phones + accepts_nested_attributes_for :phones, allow_destroy: true has_many :services, dependent: :destroy - accepts_nested_attributes_for :services + accepts_nested_attributes_for :services, allow_destroy: true # has_many :schedules, dependent: :destroy # accepts_nested_attributes_for :schedules @@ -65,11 +67,11 @@ class Location < ActiveRecord::Base # custom array validator. See app/validators/array_validator.rb validates :urls, array: { format: { with: %r{\Ahttps?://([^\s:@]+:[^\s:@]*@)?[A-Za-z\d\-]+(\.[A-Za-z\d\-]+)+\.?(:\d{1,5})?([\/?]\S*)?\z}i, - message: '%{value} is not a valid URL' } } + message: '%{value} is not a valid URL', allow_blank: true } } validates :emails, :admin_emails, array: { format: { with: /\A([^@\s]+)@((?:(?!-)[-a-z0-9]+(? 'true', 'data-dismiss' => 'modal', 'type' => 'button'} × + %h3#myModalLabel Are you ABSOLUTELY sure? +%div.modal-body + %p + = 'This action CANNOT be undone. This will delete ' + %strong + ="#{@loc_name}" + = 'and all of its associated services from ' + %strong + ="#{@org_name}" + ='permanently.' + / %p + / Please type in the name of the location to confirm. + / %p + / = text_field_tag "location-name", "", class: "span5" +%div.modal-footer + %button.btn{'aria-hidden' => 'true', 'data-dismiss' => 'modal'} Close + = link_to 'I understand the consequences, delete this location', { action: :destroy, id: @location_id }, method: :delete, class: 'btn btn-danger' diff --git a/app/views/admin/locations/_form.html.haml b/app/views/admin/locations/_form.html.haml new file mode 100644 index 000000000..4bb37ddb3 --- /dev/null +++ b/app/views/admin/locations/_form.html.haml @@ -0,0 +1,40 @@ +- if @location.errors.any? + .alert.alert-danger + %h2= "#{pluralize(@location.errors.count, "error")} prohibited this location from being saved:" + %ul + - @location.errors.full_messages.each do |msg| + %li= msg + += render 'admin/locations/forms/location_name', f: f += render 'admin/locations/forms/admin_emails', f: f += render 'admin/locations/forms/description', f: f += render 'admin/locations/forms/short_desc', f: f += render 'admin/locations/forms/address', f: f += render 'admin/locations/forms/mail_address', f: f += render 'admin/locations/forms/contacts', f: f += render 'admin/locations/forms/phones', f: f += render 'admin/locations/forms/faxes', f: f += render 'admin/locations/forms/emails', f: f += render 'admin/locations/forms/text_hours', f: f += render 'admin/locations/forms/transportation', f: f += render 'admin/locations/forms/urls', f: f += render 'admin/locations/forms/accessibility', f: f + +%div.danger-zone + %header + %strong + Danger Zone + %h4 + Delete this location + %p + Once you delete a location, there is no going back. Please be certain. + %p + = link_to 'Permanently delete this location', { action: :confirm_delete_location, location_id: @location.id, org_name: @org.name, loc_name: @location.name }, remote: true, data: { toggle: 'modal', target: '#modal-window' }, class: 'boxed-action' +%div#modal-window.modal.hide.fade{'aria-hidden' => 'true', 'aria-labelledby' => 'myModalLabel', 'role' => 'dialog'} + +%div.save-box.navbar-inner + %p + = 'Editing' + %strong + = "#{@org.name} / #{@location.name}" + = f.submit 'Save changes & apply edits to database', class: 'btn btn-success', data: { disable_with: 'Please wait...' } diff --git a/app/views/admin/locations/confirm_delete_location.js.erb b/app/views/admin/locations/confirm_delete_location.js.erb new file mode 100644 index 000000000..bbe838e73 --- /dev/null +++ b/app/views/admin/locations/confirm_delete_location.js.erb @@ -0,0 +1 @@ +$("#modal-window").html("<%= escape_javascript(render 'admin/locations/confirm_delete_location') %>"); \ No newline at end of file diff --git a/app/views/admin/locations/edit.html.haml b/app/views/admin/locations/edit.html.haml new file mode 100644 index 000000000..f7932af49 --- /dev/null +++ b/app/views/admin/locations/edit.html.haml @@ -0,0 +1,5 @@ +%div.content-box + %h2= @location.name == @org.try(:name) ? @location.name : "#{@org.try(:name)} / #{@location.name}" += form_for [:admin, @location] do |f| + = render 'form', f: f + diff --git a/app/views/admin/locations/forms/_accessibility.html.haml b/app/views/admin/locations/forms/_accessibility.html.haml new file mode 100644 index 000000000..0b766a407 --- /dev/null +++ b/app/views/admin/locations/forms/_accessibility.html.haml @@ -0,0 +1,9 @@ +%div.inst-box + %header + %strong + Accessibility Options + %p.desc + Which accessibility amenities are available at this location? + = field_set_tag nil, id: 'accessibility' do + = f.collection_check_boxes(:accessibility, Location.accessibility.options, :last, :first) do |b| + = b.label { b.check_box + b.text } diff --git a/app/views/admin/locations/forms/_address.html.haml b/app/views/admin/locations/forms/_address.html.haml new file mode 100644 index 000000000..75fc0d9e6 --- /dev/null +++ b/app/views/admin/locations/forms/_address.html.haml @@ -0,0 +1,12 @@ +%div.inst-box + %header + %strong + Street Address + %span.desc + The physical location. + = field_set_tag nil, id: 'address' do + = f.fields_for :address do |builder| + = render 'admin/locations/forms/address_fields', f: builder + = link_to 'Delete this address permanently', '#', class: 'btn btn-danger delete_association' + - unless @location.address.present? + = link_to_add_fields 'Add a street address', f, :address diff --git a/app/views/admin/locations/forms/_address_fields.html.haml b/app/views/admin/locations/forms/_address_fields.html.haml new file mode 100644 index 000000000..7fc87f5ac --- /dev/null +++ b/app/views/admin/locations/forms/_address_fields.html.haml @@ -0,0 +1,13 @@ += f.label :street, 'Street' += f.text_field :street +%br += f.label :city, 'City' += f.text_field :city, maxlength: 255 +%br += f.label :state, 'State (2-letter abbreviation)' += f.text_field :state, maxlength: 2, class: 'span1' +%br += f.label :zip, 'ZIP Code' += f.text_field :zip, maxlength: 5, class: 'span2' += f.hidden_field :_destroy +%br diff --git a/app/views/admin/locations/forms/_admin_email_fields.html.haml b/app/views/admin/locations/forms/_admin_email_fields.html.haml new file mode 100644 index 000000000..c6a6fce73 --- /dev/null +++ b/app/views/admin/locations/forms/_admin_email_fields.html.haml @@ -0,0 +1,2 @@ += text_field_tag 'location[admin_emails][]', '', class: 'span4' +%br \ No newline at end of file diff --git a/app/views/admin/locations/forms/_admin_emails.html.haml b/app/views/admin/locations/forms/_admin_emails.html.haml new file mode 100644 index 000000000..a153b5dce --- /dev/null +++ b/app/views/admin/locations/forms/_admin_emails.html.haml @@ -0,0 +1,15 @@ +- if @admin_decorator.allowed_to_access_location?(@location) + %div.inst-box + %header + %strong + Add an admin to this location + %p.desc + Which email addresses should be allowed to update and delete this location? + = field_set_tag nil, id: 'admin_emails' do + - if @location.admin_emails.present? + - @location.admin_emails.each_with_index do |admin, i| + = text_field_tag 'location[admin_emails][]', admin, class: 'span4', id: "location_admin_emails_#{i}" + %br + = link_to "Delete this admin permanently", '#', class: "btn btn-danger delete_attribute" + %p + = link_to_add_array_fields 'Add an admin email', :admin_email diff --git a/app/views/admin/locations/forms/_contact_fields.html.haml b/app/views/admin/locations/forms/_contact_fields.html.haml new file mode 100644 index 000000000..73e801325 --- /dev/null +++ b/app/views/admin/locations/forms/_contact_fields.html.haml @@ -0,0 +1,20 @@ += f.label :name, 'Name' += f.text_field :name, maxlength: 255, class: 'span5' +%br += f.label :title, 'Title' += f.text_field :title, maxlength: 255, class: 'span5' +%br += f.label :email, 'Email' += f.text_field :email, maxlength: 255, class: 'span5' +%br += f.label :phone, 'Phone' += f.text_field :phone, maxlength: 12, class: 'span2' +%br += f.label :extension, 'Extension' += f.text_field :extension, maxlength: 12, class: 'span2' +%br += f.label :fax, 'Fax' += f.text_field :fax, maxlength: 12, class: 'span2' + += f.hidden_field :_destroy +%br \ No newline at end of file diff --git a/app/views/admin/locations/forms/_contacts.html.haml b/app/views/admin/locations/forms/_contacts.html.haml new file mode 100644 index 000000000..d88f4e423 --- /dev/null +++ b/app/views/admin/locations/forms/_contacts.html.haml @@ -0,0 +1,12 @@ +%div.inst-box + %header + %strong + Contacts + %p.desc + Who are the main points of contact at the location? + = field_set_tag nil, id: 'contacts' do + = f.fields_for :contacts do |builder| + = render 'admin/locations/forms/contact_fields', f: builder + = link_to "Delete this contact permanently", '#', class: "btn btn-danger delete_association" + %p + = link_to_add_fields 'Add a contact', f, :contacts \ No newline at end of file diff --git a/app/views/admin/locations/forms/_description.html.haml b/app/views/admin/locations/forms/_description.html.haml new file mode 100644 index 000000000..816d79b61 --- /dev/null +++ b/app/views/admin/locations/forms/_description.html.haml @@ -0,0 +1,7 @@ +%div.inst-box + %header + = f.label :description, 'Description' + %span.desc + A description of the location's services. + %p + = f.text_area :description, required: true, class: 'span9', rows: 10 \ No newline at end of file diff --git a/app/views/admin/locations/forms/_email_fields.html.haml b/app/views/admin/locations/forms/_email_fields.html.haml new file mode 100644 index 000000000..913314e68 --- /dev/null +++ b/app/views/admin/locations/forms/_email_fields.html.haml @@ -0,0 +1,2 @@ += email_field_tag 'location[emails][]', '', class: 'span5' +%br diff --git a/app/views/admin/locations/forms/_emails.html.haml b/app/views/admin/locations/forms/_emails.html.haml new file mode 100644 index 000000000..1986d79fe --- /dev/null +++ b/app/views/admin/locations/forms/_emails.html.haml @@ -0,0 +1,15 @@ +%div.inst-box + %header + %strong + Emails (general info) + %p.desc + %em + If the email belongs to a contact, please move it to the existing contact, or create a new contact. + = field_set_tag nil, id: 'emails' do + - if @location.emails.present? + - @location.emails.each_with_index do |email, i| + = email_field_tag 'location[emails][]', email, class: 'span6', id: "location_emails_#{i}" + %br + = link_to "Delete this email permanently", '#', class: "btn btn-danger delete_attribute" + %p + = link_to_add_array_fields 'Add a general email', :email diff --git a/app/views/admin/locations/forms/_fax_fields.html.haml b/app/views/admin/locations/forms/_fax_fields.html.haml new file mode 100644 index 000000000..0c11b7e58 --- /dev/null +++ b/app/views/admin/locations/forms/_fax_fields.html.haml @@ -0,0 +1,8 @@ += f.label :name, 'Number' += f.text_field :number, maxlength: 12, class: 'span2' +%br += f.label :department, 'Department' += f.text_field :department, maxlength: 50, class: 'span4' + += f.hidden_field :_destroy +%br \ No newline at end of file diff --git a/app/views/admin/locations/forms/_faxes.html.haml b/app/views/admin/locations/forms/_faxes.html.haml new file mode 100644 index 000000000..3ddf8432b --- /dev/null +++ b/app/views/admin/locations/forms/_faxes.html.haml @@ -0,0 +1,15 @@ +%div.inst-box + %header + %strong + Fax Numbers + %p.desc + %em + 10 digits only please, in this format: 650-802-7922. + %br + If the fax number belongs to a contact, please move it to the existing contact, or create a new contact. + = field_set_tag nil, id: 'faxes' do + = f.fields_for :faxes do |builder| + = render 'admin/locations/forms/fax_fields', f: builder + = link_to "Delete this fax permanently", '#', class: "btn btn-danger delete_association" + %p + = link_to_add_fields 'Add a fax number', f, :faxes diff --git a/app/views/admin/locations/forms/_hours.html.haml b/app/views/admin/locations/forms/_hours.html.haml new file mode 100644 index 000000000..690dfd331 --- /dev/null +++ b/app/views/admin/locations/forms/_hours.html.haml @@ -0,0 +1,22 @@ +%div.inst-box + %header + %strong + Opening Hours: + Select the opening and closing times for this location + %p + - i = 0 + - days.each do |day| + = "#{day}" + = select_tag("#{day}_first_open", options_for_select(create_hours(), @location.schedule.present? ? @location.schedule[i].open : "09:00")) + = "---" + = select_tag("#{day}_first_close", options_for_select(create_hours(), @location.schedule.present? ? @location.schedule[i].close : "12:00")) + %br + - if @location.schedule.present? && @location.schedule[i+1].open.present? + = "and" + = "#{day}" + = select_tag("#{day}_sec_open", options_for_select([""]+create_hours(), @location.schedule.present? ? @location.schedule[i+1].open : "")) + = "---" + = select_tag("#{day}_sec_close", options_for_select([""]+create_hours(), @location.schedule.present? ? @location.schedule[i+1].close : "")) + - i += 2 + %br + %br \ No newline at end of file diff --git a/app/views/admin/locations/forms/_location_name.html.haml b/app/views/admin/locations/forms/_location_name.html.haml new file mode 100644 index 000000000..e80ae554a --- /dev/null +++ b/app/views/admin/locations/forms/_location_name.html.haml @@ -0,0 +1,7 @@ +%div.inst-box + %header + = f.label :name, 'Location Name' + %span.desc + The name of the organization at a location, such as a branch, department, or similar. + %p + = f.text_field :name, required: true, maxlength: 255, class: 'span10' \ No newline at end of file diff --git a/app/views/admin/locations/forms/_mail_address.html.haml b/app/views/admin/locations/forms/_mail_address.html.haml new file mode 100644 index 000000000..c52539e29 --- /dev/null +++ b/app/views/admin/locations/forms/_mail_address.html.haml @@ -0,0 +1,10 @@ +%div.inst-box + %header + %strong + Mailing Address + = field_set_tag nil, id: 'mail_address' do + = f.fields_for :mail_address do |builder| + = render 'admin/locations/forms/mail_address_fields', f: builder + = link_to 'Delete this mailing address permanently', '#', class: 'btn btn-danger delete_association' + - unless @location.mail_address.present? + = link_to_add_fields 'Add a mailing address', f, :mail_address diff --git a/app/views/admin/locations/forms/_mail_address_fields.html.haml b/app/views/admin/locations/forms/_mail_address_fields.html.haml new file mode 100644 index 000000000..35f3e3f14 --- /dev/null +++ b/app/views/admin/locations/forms/_mail_address_fields.html.haml @@ -0,0 +1,3 @@ += f.label :attention, 'Attention' += f.text_field :attention, maxlength: 255, class: 'span5' += render 'admin/locations/forms/address_fields', f: f \ No newline at end of file diff --git a/app/views/admin/locations/forms/_new_location_form.html.haml b/app/views/admin/locations/forms/_new_location_form.html.haml new file mode 100644 index 000000000..590724bf8 --- /dev/null +++ b/app/views/admin/locations/forms/_new_location_form.html.haml @@ -0,0 +1,23 @@ +- if @location.errors.any? + .alert.alert-danger + %h2= "#{pluralize(@location.errors.count, "error")} prohibited this location from being saved:" + %ul + - @location.errors.full_messages.each do |msg| + %li= msg + += render 'admin/locations/forms/location_name', f: f += render 'admin/locations/forms/admin_emails', f: f += render 'admin/locations/forms/description', f: f += render 'admin/locations/forms/short_desc', f: f += render 'admin/locations/forms/address', f: f += render 'admin/locations/forms/mail_address', f: f += render 'admin/locations/forms/contacts', f: f += render 'admin/locations/forms/urls', f: f, location_url: @location_url += f.hidden_field :organization_id, value: @org.id + +%div.save-box.navbar-inner + %p + = 'Creating location for' + %strong + = @org.name + = f.submit 'Create location', class: 'btn btn-primary', data: { disable_with: 'Please wait...' } diff --git a/app/views/admin/locations/forms/_phone_fields.html.haml b/app/views/admin/locations/forms/_phone_fields.html.haml new file mode 100644 index 000000000..59d060109 --- /dev/null +++ b/app/views/admin/locations/forms/_phone_fields.html.haml @@ -0,0 +1,14 @@ += f.label :number, 'Number' += f.text_field :number, maxlength: 12, class: 'span2' +%br += f.label :vanity_number, 'Vanity Number (for example: 650-123-HELP)' += f.text_field :vanity_number, maxlength: 12, class: 'span2' +%br += f.label :extension, 'Extension' += f.text_field :extension, maxlength: 8, class: 'span2' +%br += f.label :department, 'Department' += f.text_field :department, maxlength: 50, class: 'span4' + += f.hidden_field :_destroy +%br \ No newline at end of file diff --git a/app/views/admin/locations/forms/_phones.html.haml b/app/views/admin/locations/forms/_phones.html.haml new file mode 100644 index 000000000..50312cd6b --- /dev/null +++ b/app/views/admin/locations/forms/_phones.html.haml @@ -0,0 +1,15 @@ +%div.inst-box + %header + %strong + Phone Numbers + %p.desc + %em + 10 digits only please, in this format: 650-802-7922. + %br + If the phone number belongs to a contact, please move it to the existing contact, or create a new contact. + = field_set_tag nil, id: 'phones' do + = f.fields_for :phones do |builder| + = render 'admin/locations/forms/phone_fields', f: builder + = link_to "Delete this phone permanently", '#', class: "btn btn-danger delete_association" + %p + = link_to_add_fields 'Add a phone number', f, :phones diff --git a/app/views/admin/locations/forms/_short_desc.html.haml b/app/views/admin/locations/forms/_short_desc.html.haml new file mode 100644 index 000000000..fa8bd2dab --- /dev/null +++ b/app/views/admin/locations/forms/_short_desc.html.haml @@ -0,0 +1,7 @@ +%div.inst-box + %header + = f.label :short_desc, 'Short Description' + %span.desc + A short summary of the description of services. + %p + = f.text_area :short_desc, class: 'span9' \ No newline at end of file diff --git a/app/views/admin/locations/forms/_text_hours.html.haml b/app/views/admin/locations/forms/_text_hours.html.haml new file mode 100644 index 000000000..54b0c0e77 --- /dev/null +++ b/app/views/admin/locations/forms/_text_hours.html.haml @@ -0,0 +1,9 @@ +%div.inst-box + %header + = f.label :hours, 'Hours of operation' + %span.desc + When is the location open? + %em + For example, "Monday-Friday, 9-12, 2-5" + %p + = f.text_field :hours, class: 'span11' \ No newline at end of file diff --git a/app/views/admin/locations/forms/_transportation.html.haml b/app/views/admin/locations/forms/_transportation.html.haml new file mode 100644 index 000000000..68cb43ec2 --- /dev/null +++ b/app/views/admin/locations/forms/_transportation.html.haml @@ -0,0 +1,7 @@ +%div.inst-box + %header + = f.label :transportation, 'Transportation Options' + %span.desc + What public transportation options are nearby? (Bus stops, train stations, etc.) + %p + = f.text_area :transportation, class: 'span9', maxlength: 255 \ No newline at end of file diff --git a/app/views/admin/locations/forms/_url_fields.html.haml b/app/views/admin/locations/forms/_url_fields.html.haml new file mode 100644 index 000000000..33f6b2c02 --- /dev/null +++ b/app/views/admin/locations/forms/_url_fields.html.haml @@ -0,0 +1,2 @@ += url_field_tag 'location[urls][]', '', class: 'span9' +%br diff --git a/app/views/admin/locations/forms/_urls.html.haml b/app/views/admin/locations/forms/_urls.html.haml new file mode 100644 index 000000000..0c4af78c8 --- /dev/null +++ b/app/views/admin/locations/forms/_urls.html.haml @@ -0,0 +1,19 @@ +%div.inst-box + %header + %strong + Websites + %p.desc + What websites are associated with the location? + %em + Must include "http://" or "https://" + = field_set_tag nil, id: 'urls' do + - if f.object.new_record? + = url_field_tag "location[urls][]", @location_url, class: 'span9' + - elsif @location.urls.present? + - @location.urls.each_with_index do |url, i| + = url_field_tag 'location[urls][]', url, class: 'span9', id: "location_urls_#{i}" + %br + = link_to "Delete this website permanently", '#', class: "btn btn-danger delete_attribute" + %p + = link_to_add_array_fields 'Add a website', :url + diff --git a/app/views/admin/locations/new.html.haml b/app/views/admin/locations/new.html.haml new file mode 100644 index 000000000..c537827f6 --- /dev/null +++ b/app/views/admin/locations/new.html.haml @@ -0,0 +1,5 @@ +%div.content-box + %h1 Create a new location + += form_for [:admin, @location], url: admin_locations_path, :html => {:method => :post} do |f| + = render 'admin/locations/forms/new_location_form', f: f diff --git a/app/views/shared/_messages.html.haml b/app/views/shared/_messages.html.haml index 80e34d462..b924fc708 100644 --- a/app/views/shared/_messages.html.haml +++ b/app/views/shared/_messages.html.haml @@ -1,5 +1,5 @@ - flash.each do |name, msg| - if msg.is_a?(String) - %div{:class => "alert alert-#{name == :notice ? "success" : "error"}"} + %div{class: "alert alert-#{name.to_s == 'notice' ? 'success' : 'error'}"} %a.close{"data-dismiss" => "alert"} × - = content_tag :div, msg, :id => "flash_#{name}" + = content_tag :div, msg, :id => "flash_#{name.to_s}" diff --git a/config/routes.rb b/config/routes.rb index 77d26c6c2..eab132002 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -13,7 +13,8 @@ namespace :admin do root to: 'dashboard#index', as: :dashboard resources :locations, except: :show - get 'locations/:id', to: 'admin/locations#edit' + get 'locations/confirm_delete_location', to: 'locations#confirm_delete_location', as: :confirm_delete_location + get 'locations/:id', to: 'locations#edit' end devise_for :admins, path: 'admin', controllers: { registrations: 'admin/registrations' } diff --git a/spec/api/patch_location_spec.rb b/spec/api/patch_location_spec.rb index 2feb4b246..2216e369c 100644 --- a/spec/api/patch_location_spec.rb +++ b/spec/api/patch_location_spec.rb @@ -36,8 +36,7 @@ patch api_location_url(@loc, subdomain: ENV['API_SUBDOMAIN']), emails: '' expect(response.status).to eq(422) expect(json['message']).to eq('Validation failed for resource.') - expect(json['errors'].first['emails'].first). - to eq(' is not a valid email') + expect(json['error']).to include('Attribute was supposed to be an Array') end it 'returns 422 when attribute is invalid' do diff --git a/spec/factories/admins.rb b/spec/factories/admins.rb index 56227e195..b7e27dade 100644 --- a/spec/factories/admins.rb +++ b/spec/factories/admins.rb @@ -19,4 +19,12 @@ password 'ohanatest' password_confirmation 'ohanatest' end + + factory :admin_with_generic_email, class: :admin do + name 'Generic User' + email 'moncef@gmail.com' + password 'ohanatest' + password_confirmation 'ohanatest' + confirmed_at Time.now + end end diff --git a/spec/features/admin/dashboard_spec.rb b/spec/features/admin/dashboard_spec.rb index 0e3b47603..17cd5c2bf 100644 --- a/spec/features/admin/dashboard_spec.rb +++ b/spec/features/admin/dashboard_spec.rb @@ -1,8 +1,6 @@ require 'rails_helper' feature 'Admin Home page' do - include Warden::Test::Helpers - context 'when not signed in' do before :each do visit '/admin' @@ -55,15 +53,10 @@ context 'when signed in' do before :each do - Warden.test_mode! login_admin visit '/admin' end - after :each do - Warden.test_reset! - end - it 'greets the admin by their name' do expect(page).to have_content 'Welcome back, Org Admin!' end diff --git a/spec/features/admin/locations/update_accessibility_spec.rb b/spec/features/admin/locations/update_accessibility_spec.rb new file mode 100644 index 000000000..a22fc9c20 --- /dev/null +++ b/spec/features/admin/locations/update_accessibility_spec.rb @@ -0,0 +1,54 @@ +require 'rails_helper' + +feature "Update a location's accessibility options" do + before(:all) do + @location = create(:soup_kitchen) + end + + before(:each) do + @location.reload + login_super_admin + visit '/admin/locations/soup-kitchen' + end + + after(:all) do + Organization.find_each(&:destroy) + end + + scenario "when location doesn't have any options" do + all('input[type=checkbox]').each do |checkbox| + expect(checkbox).to_not be_checked + end + end + + scenario 'when adding an option' do + check 'location_accessibility_elevator' + click_button 'Save changes' + expect(page). + to have_content 'Location was successfully updated.' + expect(find('#location_accessibility_elevator')).to be_checked + reset_accessibility + end + + scenario 'when removing an option' do + check 'location_accessibility_restroom' + click_button 'Save changes' + visit '/admin/locations/soup-kitchen' + uncheck 'location_accessibility_restroom' + click_button 'Save changes' + expect(page). + to have_content 'Location was successfully updated.' + expect(find('#location_accessibility_restroom')).to_not be_checked + end + + scenario 'when adding all options' do + set_all_accessibility + visit '/admin/locations/soup-kitchen' + within_fieldset('accessibility') do + all('input[type=checkbox]').each do |checkbox| + expect(checkbox).to be_checked unless checkbox[:id].match('accessibility').nil? + end + end + reset_accessibility + end +end diff --git a/spec/features/admin/locations/update_address_spec.rb b/spec/features/admin/locations/update_address_spec.rb new file mode 100644 index 000000000..491e7139b --- /dev/null +++ b/spec/features/admin/locations/update_address_spec.rb @@ -0,0 +1,80 @@ +require 'rails_helper' + +feature "Updating a location's address" do + before(:each) do + @location = create(:no_address) + login_super_admin + visit '/admin/locations/no-address' + end + + scenario 'adding a new street address with valid values', :js do + add_street_address(street: '123', city: 'Vienn', state: 'VA', zip: '12345') + visit '/admin/locations/no-address' + + expect(find_field('location_address_attributes_street').value).to eq '123' + expect(find_field('location_address_attributes_city').value).to eq 'Vienn' + expect(find_field('location_address_attributes_state').value).to eq 'VA' + expect(find_field('location_address_attributes_zip').value).to eq '12345' + + remove_street_address + visit '/admin/locations/no-address' + expect(page).to have_link 'Add a street address' + end + + scenario 'when leaving location without address or mail address', :js do + remove_mail_address + expect(page). + to have_content 'A location must have at least one address type' + end +end + +feature "Updating a location's address with invalid values" do + before(:all) do + @location = create(:location) + end + + before(:each) do + login_super_admin + visit '/admin/locations/vrs-services' + end + + after(:all) do + Organization.find_each(&:destroy) + end + + scenario 'with an empty street' do + update_street_address(street: '', city: 'fair', state: 'VA', zip: '12345') + click_button 'Save changes' + expect(page).to have_content "street can't be blank for Address" + end + + scenario 'with an empty city' do + update_street_address(street: '123', city: '', state: 'VA', zip: '12345') + click_button 'Save changes' + expect(page).to have_content "city can't be blank for Address" + end + + scenario 'with an empty state' do + update_street_address(street: '123', city: 'fair', state: '', zip: '12345') + click_button 'Save changes' + expect(page).to have_content "state can't be blank for Address" + end + + scenario 'with an empty zip' do + update_street_address(street: '123', city: 'Belmont', state: 'CA', zip: '') + click_button 'Save changes' + expect(page).to have_content "zip can't be blank for Address" + end + + scenario 'with an invalid state' do + update_street_address(street: '123', city: 'Par', state: 'V', zip: '12345') + click_button 'Save changes' + expect(page).to have_content 'valid 2-letter state abbreviation' + end + + scenario 'with an invalid zip' do + update_street_address(street: '123', city: 'Ald', state: 'VA', zip: '1234') + click_button 'Save changes' + expect(page).to have_content 'valid ZIP code' + end +end diff --git a/spec/features/admin/locations/update_contacts_spec.rb b/spec/features/admin/locations/update_contacts_spec.rb new file mode 100644 index 000000000..9e09f932e --- /dev/null +++ b/spec/features/admin/locations/update_contacts_spec.rb @@ -0,0 +1,142 @@ +require 'rails_helper' + +feature 'Update contacts' do + background do + create(:location) + login_super_admin + visit '/admin/locations/vrs-services' + end + + scenario 'when no contacts exist' do + within('#contacts') do + expect(page). + to have_no_xpath('.//input[contains(@name, "[name]")]') + end + end + + scenario 'by adding a new contact', :js do + add_contact( + name: 'Moncef Belyamani-Belyamani', + title: 'Director of Development and Operations', + email: 'moncefbelyamani@samaritanhousesanmateo.org', + phone: '703-555-1212', + extension: 'x1234', + fax: '703-555-1234' + ) + click_button 'Save changes' + visit '/admin/locations/vrs-services' + + expect(find_field('location_contacts_attributes_0_name').value). + to eq 'Moncef Belyamani-Belyamani' + + expect(find_field('location_contacts_attributes_0_title').value). + to eq 'Director of Development and Operations' + + expect(find_field('location_contacts_attributes_0_email').value). + to eq 'moncefbelyamani@samaritanhousesanmateo.org' + + expect(find_field('location_contacts_attributes_0_phone').value). + to eq '703-555-1212' + + expect(find_field('location_contacts_attributes_0_extension').value). + to eq 'x1234' + + expect(find_field('location_contacts_attributes_0_fax').value).to eq '703-555-1234' + + delete_contact + visit '/admin/locations/vrs-services' + within('#contacts') do + expect(page). + to have_no_xpath('.//input[contains(@name, "[name]")]') + end + end + + scenario 'with 2 contacts but one is empty', :js do + add_contact( + name: 'Moncef Belyamani-Belyamani', + title: 'Director of Development and Operations' + ) + click_link 'Add a contact' + click_button 'Save changes' + visit '/admin/locations/vrs-services' + + within('#contacts') do + total_contacts = all(:xpath, './/input[contains(@name, "[name]")]') + expect(total_contacts.length).to eq 1 + end + end + + scenario 'with 2 contacts but second one is invalid', :js do + add_contact( + name: 'Moncef Belyamani-Belyamani', + title: 'Director of Development and Operations' + ) + click_link 'Add a contact' + within('#contacts') do + all_phones = all(:xpath, './/input[contains(@name, "[phone]")]') + fill_in all_phones[-1][:id], with: '202-555-1212' + end + click_button 'Save changes' + expect(page).to have_content "name can't be blank for Contact" + end +end + +feature 'Update contacts' do + before(:all) do + @location = create(:location) + @location.contacts.create!(name: 'foo', title: 'bar') + end + + before(:each) do + login_super_admin + visit '/admin/locations/vrs-services' + end + + scenario 'with an empty name' do + update_contact(name: '') + click_button 'Save changes' + expect(page).to have_content "name can't be blank for Contact" + end + + scenario 'with an empty title' do + update_contact(title: '') + click_button 'Save changes' + expect(page).to have_content "title can't be blank for Contact" + end + + scenario 'with an empty email' do + update_contact(email: '') + click_button 'Save changes' + expect(page).to_not have_content 'is not a valid email' + end + + scenario 'with an empty phone' do + update_contact(phone: '') + click_button 'Save changes' + expect(page).to_not have_content 'is not a valid US phone number' + end + + scenario 'with an empty fax' do + update_contact(fax: '') + click_button 'Save changes' + expect(page).to_not have_content 'is not a valid US fax number' + end + + scenario 'with an invalid email' do + update_contact(email: '703') + click_button 'Save changes' + expect(page).to have_content 'is not a valid email' + end + + scenario 'with an invalid phone' do + update_contact(phone: '703') + click_button 'Save changes' + expect(page).to have_content 'is not a valid US phone number' + end + + scenario 'with an invalid fax' do + update_contact(fax: '202') + click_button 'Save changes' + expect(page).to have_content 'is not a valid US fax number' + end +end diff --git a/spec/features/admin/locations/visit_location_spec.rb b/spec/features/admin/locations/visit_location_spec.rb new file mode 100644 index 000000000..65bade606 --- /dev/null +++ b/spec/features/admin/locations/visit_location_spec.rb @@ -0,0 +1,72 @@ +require 'rails_helper' + +feature 'Visiting a specific location' do + before(:all) do + @location = create(:location) + end + + before(:each) do + @location.reload + end + + after(:all) do + Organization.find_each(&:destroy) + end + + scenario "when location doesn't include generic email" do + admin = create(:admin_with_generic_email) + login_as_admin(admin) + visit('/admin/locations/vrs-services') + expect(page).to have_content "Sorry, you don't have access to that page" + expect(current_path).to eq(admin_dashboard_path) + end + + scenario "when location doesn't include domain name" do + login_admin + visit('/admin/locations/vrs-services') + expect(page).to have_content "Sorry, you don't have access to that page" + expect(current_path).to eq(admin_dashboard_path) + end + + scenario 'when location includes domain name' do + @location.update!(urls: ['http://samaritanhouse.com']) + login_admin + visit('/admin/locations/vrs-services') + expect(page).to_not have_content "Sorry, you don't have access to that page" + @location.update!(urls: []) + end + + scenario 'when admin is location admin' do + new_admin = create(:admin_with_generic_email) + @location.update!(admin_emails: [new_admin.email]) + login_as_admin(new_admin) + visit('/admin/locations/vrs-services') + expect(page).to_not have_content "Sorry, you don't have access to that page" + @location.update!(admin_emails: []) + end + + scenario 'when admin is location admin but has non-generic email' do + login_admin + @location.update!(admin_emails: [@admin.email]) + visit('/admin/locations/vrs-services') + expect(page).to_not have_content "Sorry, you don't have access to that page" + @location.update!(admin_emails: []) + end + + scenario 'when admin is super admin' do + login_super_admin + visit('/admin/locations/vrs-services') + expect(page).to_not have_content "Sorry, you don't have access to that page" + end + + context "when admin doesn't belong to any locations" do + it 'denies access to create a new location' do + login_admin + visit('/admin/locations/new') + expect(page).to have_content "Sorry, you don't have access to that page" + expect(current_path).to eq(admin_dashboard_path) + visit('/admin/locations') + expect(page).to_not have_link 'Add a new location' + end + end +end diff --git a/spec/features/admin/visit_locations_spec.rb b/spec/features/admin/locations/visit_locations_spec.rb similarity index 95% rename from spec/features/admin/visit_locations_spec.rb rename to spec/features/admin/locations/visit_locations_spec.rb index 13d870133..f612fe3a6 100644 --- a/spec/features/admin/visit_locations_spec.rb +++ b/spec/features/admin/locations/visit_locations_spec.rb @@ -1,8 +1,6 @@ require 'rails_helper' feature 'Locations page' do - include Warden::Test::Helpers - context 'when not signed in' do before :each do visit '/admin/locations' @@ -44,15 +42,10 @@ context 'when signed in' do before :each do - Warden.test_mode! login_admin visit '/admin/locations' end - after :each do - Warden.test_reset! - end - it 'displays instructions for editing locations' do expect(page).to have_content 'Below you should see a list of locations' expect(page).to have_content 'To start updating, click on one of the links' @@ -111,15 +104,10 @@ context 'when signed in as super admin' do before :each do - Warden.test_mode! login_super_admin visit '/admin/locations' end - after :each do - Warden.test_reset! - end - it 'displays instructions for editing locations' do expect(page).to have_content 'As a super admin' end diff --git a/spec/features/admin/sign_out_spec.rb b/spec/features/admin/sign_out_spec.rb index 748a360dc..5420d191c 100644 --- a/spec/features/admin/sign_out_spec.rb +++ b/spec/features/admin/sign_out_spec.rb @@ -1,18 +1,11 @@ require 'rails_helper' feature 'Signing out' do - include Warden::Test::Helpers - background do - Warden.test_mode! login_admin visit edit_admin_registration_path end - after(:each) do - Warden.test_reset! - end - it 'redirects to the admin home page' do click_link 'Sign out' expect(current_path).to eq(admin_dashboard_path) diff --git a/spec/features/create_new_api_application_spec.rb b/spec/features/create_new_api_application_spec.rb index a0804b036..e1b772a37 100644 --- a/spec/features/create_new_api_application_spec.rb +++ b/spec/features/create_new_api_application_spec.rb @@ -1,7 +1,6 @@ require 'rails_helper' feature 'Creating a new API Application' do - include Warden::Test::Helpers # The 'create_api_app' method is defined in # spec/support/features/session_helpers.rb @@ -15,16 +14,11 @@ # All other methods are part of the Capybara DSL # https://github.com/jnicklas/capybara background do - Warden.test_mode! user = FactoryGirl.create(:user) login_as(user, scope: :user) visit '/api_applications' end - after(:each) do - Warden.test_reset! - end - scenario 'visit apps with no apps created' do expect(page).to_not have_content 'http' end diff --git a/spec/features/homepage_text_spec.rb b/spec/features/homepage_text_spec.rb index 36f555327..51db7504f 100644 --- a/spec/features/homepage_text_spec.rb +++ b/spec/features/homepage_text_spec.rb @@ -1,7 +1,6 @@ require 'rails_helper' feature 'Visit home page after signing in' do - include Warden::Test::Helpers # The 'login_as' method is a Warden test helper that # allows you to simulate a user login without having # to fill in the sign in form every time. Since we're @@ -12,16 +11,11 @@ # All other methods are part of the Capybara DSL # https://github.com/jnicklas/capybara background do - Warden.test_mode! user = FactoryGirl.create(:user) login_as(user, scope: :user) visit '/' end - after(:each) do - Warden.test_reset! - end - it 'includes a link to the Docs page in the navigation' do within '.navbar' do expect(page).to have_link 'Docs', href: docs_path diff --git a/spec/features/sign_out_spec.rb b/spec/features/sign_out_spec.rb index 20f95a083..d144e3a52 100644 --- a/spec/features/sign_out_spec.rb +++ b/spec/features/sign_out_spec.rb @@ -1,18 +1,11 @@ require 'rails_helper' feature 'Signing out' do - include Warden::Test::Helpers - background do - Warden.test_mode! login_user visit edit_user_registration_path end - after(:each) do - Warden.test_reset! - end - it 'redirects to the user home page' do click_link 'Sign out' expect(current_path).to eq(root_path) diff --git a/spec/features/update_api_application_spec.rb b/spec/features/update_api_application_spec.rb index 2fa0e1b46..4ff67b0d7 100644 --- a/spec/features/update_api_application_spec.rb +++ b/spec/features/update_api_application_spec.rb @@ -1,7 +1,6 @@ require 'rails_helper' feature 'Update an existing API Application' do - include Warden::Test::Helpers # The 'visit_app' and 'update_api_app' methods are defined in # spec/support/features/session_helpers.rb @@ -15,7 +14,6 @@ # All other methods are part of the Capybara DSL # https://github.com/jnicklas/capybara background do - Warden.test_mode! user = FactoryGirl.create(:user_with_app) login_as(user, scope: :user) name = user.api_applications.first.name @@ -23,10 +21,6 @@ visit_app(name, main_url) end - after(:each) do - Warden.test_reset! - end - scenario 'with valid fields' do update_api_app('my awesome app', 'http://cfa.org', 'http://cfa.org') expect(page).to have_content 'Application was successfully updated.' diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index f069d3bf4..c59ea5bf4 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -8,6 +8,11 @@ require 'rspec/rails' require 'shoulda/matchers' +require 'capybara/poltergeist' +Capybara.javascript_driver = :poltergeist + +Rails.logger.level = 4 + # Requires supporting ruby files with custom matchers and macros, etc, in # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are # run as spec files by default. This means that files in spec/support that end @@ -23,32 +28,10 @@ RSpec.configure do |config| config.include FactoryGirl::Syntax::Methods config.include Features::SessionHelpers, type: :feature + config.include Features::FormHelpers, type: :feature config.include Requests::RequestHelpers, type: :request config.include DefaultHeaders, type: :request - config.before(:suite) do - DatabaseCleaner.clean_with(:truncation) - end - - config.before(:each) do - DatabaseCleaner.strategy = :transaction - end - - config.before(:each) do - DatabaseCleaner.start - Bullet.start_request if Bullet.enable? - end - - config.after(:each) do - DatabaseCleaner.clean - Bullet.end_request if Bullet.enable? - end - - # config.after(:all) do - # DatabaseCleaner.clean_with(:truncation) - # Bullet.end_request if Bullet.enable? - # end - # rspec-rails 3+ will no longer automatically infer an example group's spec # type from the file location. You can explicitly opt-in to this feature by # uncommenting the setting below. @@ -60,11 +43,6 @@ # If you're not using ActiveRecord, or you'd prefer not to run each of your # examples within a transaction, remove the following line or assign false # instead of true. - # config.use_transactional_fixtures = true + config.use_transactional_fixtures = false # require 'active_record_spec_helper' - - # If true, the base class of anonymous controllers will be inferred - # automatically. This will be the default behavior in future versions of - # rspec-rails. - # config.infer_base_class_for_anonymous_controllers = false end diff --git a/spec/support/bullet.rb b/spec/support/bullet.rb new file mode 100644 index 000000000..98b0f2326 --- /dev/null +++ b/spec/support/bullet.rb @@ -0,0 +1,10 @@ +RSpec.configure do |config| + + config.before(:each) do + Bullet.start_request if Bullet.enable? + end + + config.after(:each) do + Bullet.end_request if Bullet.enable? + end +end diff --git a/spec/support/database_cleaner.rb b/spec/support/database_cleaner.rb new file mode 100644 index 000000000..284790b7f --- /dev/null +++ b/spec/support/database_cleaner.rb @@ -0,0 +1,22 @@ +RSpec.configure do |config| + + config.before(:suite) do + DatabaseCleaner.clean_with(:truncation) + end + + config.before(:each) do + DatabaseCleaner.strategy = :transaction + end + + config.before(:each, js: true) do + DatabaseCleaner.strategy = :truncation + end + + config.before(:each) do + DatabaseCleaner.start + end + + config.after(:each) do + DatabaseCleaner.clean + end +end diff --git a/spec/support/features/form_helpers.rb b/spec/support/features/form_helpers.rb new file mode 100644 index 000000000..431015933 --- /dev/null +++ b/spec/support/features/form_helpers.rb @@ -0,0 +1,66 @@ +module Features + module FormHelpers + def reset_accessibility + within_fieldset('accessibility') do + all('input[type=checkbox]').each do |checkbox| + uncheck checkbox[:id] + end + end + click_button 'Save changes' + end + + def set_all_accessibility + within_fieldset('accessibility') do + all('input[type=checkbox]').each do |checkbox| + check checkbox[:id] + end + end + click_button 'Save changes' + end + + def add_street_address(options = {}) + click_link 'Add a street address' + update_street_address(options) + click_button 'Save changes' + end + + def update_street_address(options = {}) + fill_in 'location_address_attributes_street', with: options[:street] + fill_in 'location_address_attributes_city', with: options[:city] + fill_in 'location_address_attributes_state', with: options[:state] + fill_in 'location_address_attributes_zip', with: options[:zip] + click_button 'Save changes' + end + + def remove_street_address + click_link 'Delete this address permanently' + click_button 'Save changes' + end + + def remove_mail_address + click_link 'Delete this mailing address permanently' + click_button 'Save changes' + end + + def add_contact(options = {}) + click_link 'Add a contact' + update_contact(options) + end + + def update_contact(options = {}) + within('#contacts') do + fill_in find(:xpath, './/input[contains(@name, "[name]")]')[:id], with: options[:name] + fill_in find(:xpath, './/input[contains(@name, "[title]")]')[:id], with: options[:title] + fill_in find(:xpath, './/input[contains(@name, "[email]")]')[:id], with: options[:email] + fill_in find(:xpath, './/input[contains(@name, "[phone]")]')[:id], with: options[:phone] + fill_in find(:xpath, './/input[contains(@name, "[fax]")]')[:id], with: options[:fax] + fill_in find(:xpath, './/input[contains(@name, "[extension]")]')[:id], with: options[:extension] + end + end + + def delete_contact + click_link 'Delete this contact permanently' + click_button 'Save changes' + end + end +end diff --git a/spec/support/features/session_helpers.rb b/spec/support/features/session_helpers.rb index 721d239f3..6293ea57e 100644 --- a/spec/support/features/session_helpers.rb +++ b/spec/support/features/session_helpers.rb @@ -7,6 +7,10 @@ def login_admin login_as(@admin, scope: :admin) end + def login_as_admin(admin) + login_as(admin, scope: :admin) + end + def login_super_admin @super_admin = FactoryGirl.create(:super_admin) login_as(@super_admin, scope: :admin) diff --git a/spec/support/warden.rb b/spec/support/warden.rb new file mode 100644 index 000000000..0af2ffd22 --- /dev/null +++ b/spec/support/warden.rb @@ -0,0 +1,11 @@ +RSpec.configure do |config| + config.include Warden::Test::Helpers, type: :feature + + config.before(:each) do + Warden.test_mode! + end + + config.after(:each) do + Warden.test_reset! + end +end From aaf8f290099a5866f1faffbf56d23e5544236c62 Mon Sep 17 00:00:00 2001 From: Moncef Belyamani Date: Thu, 3 Jul 2014 22:51:41 -0400 Subject: [PATCH 05/17] Update Rubocop to version 0.24.1 --- Gemfile.lock | 6 +++--- spec/features/admin/locations/update_contacts_spec.rb | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 0e7438697..822bbfa56 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -126,7 +126,7 @@ GEM nokogiri (1.6.2.1) mini_portile (= 0.6.0) orm_adapter (0.5.0) - parser (2.2.0.pre.2) + parser (2.2.0.pre.3) ast (>= 1.1, < 3.0) slop (~> 3.4, >= 3.4.5) passenger (4.0.45) @@ -199,9 +199,9 @@ GEM rspec-mocks (~> 3.0.0) rspec-support (~> 3.0.0) rspec-support (3.0.0) - rubocop (0.24.0) + rubocop (0.24.1) json (>= 1.7.7, < 2) - parser (>= 2.2.0.pre.2, < 3.0) + parser (>= 2.2.0.pre.3, < 3.0) powerpack (~> 0.0.6) rainbow (>= 1.99.1, < 3.0) ruby-progressbar (~> 1.4) diff --git a/spec/features/admin/locations/update_contacts_spec.rb b/spec/features/admin/locations/update_contacts_spec.rb index 9e09f932e..69e7c34e8 100644 --- a/spec/features/admin/locations/update_contacts_spec.rb +++ b/spec/features/admin/locations/update_contacts_spec.rb @@ -92,6 +92,10 @@ visit '/admin/locations/vrs-services' end + after(:all) do + Organization.find_each(&:destroy) + end + scenario 'with an empty name' do update_contact(name: '') click_button 'Save changes' From 476356605d1ef424f2a429c85411f1a18089e536 Mon Sep 17 00:00:00 2001 From: Moncef Belyamani Date: Thu, 3 Jul 2014 23:26:47 -0400 Subject: [PATCH 06/17] Update bullet to version 4.11.3 --- Gemfile.lock | 2 +- spec/support/bullet.rb | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 822bbfa56..12e0c322c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -44,7 +44,7 @@ GEM bootstrap-sass (2.3.2.2) sass (~> 3.2) builder (3.2.2) - bullet (4.11.2) + bullet (4.11.3) activesupport (>= 3.0.0) uniform_notifier (>= 1.6.0) capybara (2.3.0) diff --git a/spec/support/bullet.rb b/spec/support/bullet.rb index 98b0f2326..2c1959892 100644 --- a/spec/support/bullet.rb +++ b/spec/support/bullet.rb @@ -1,10 +1,13 @@ RSpec.configure do |config| - config.before(:each) do - Bullet.start_request if Bullet.enable? - end + if Bullet.enable? + config.before(:each) do + Bullet.start_request + end - config.after(:each) do - Bullet.end_request if Bullet.enable? + config.after(:each) do + # Bullet.perform_out_of_channel_notifications if Bullet.notification? + Bullet.end_request + end end end From 769e197d57bfe213ac8c15e1800fad6bc6ed5540 Mon Sep 17 00:00:00 2001 From: Moncef Belyamani Date: Thu, 3 Jul 2014 23:28:44 -0400 Subject: [PATCH 07/17] Update Rails to version 4.1.4 --- Gemfile.lock | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 12e0c322c..4ff701684 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,29 +1,29 @@ GEM remote: https://rubygems.org/ specs: - actionmailer (4.1.2) - actionpack (= 4.1.2) - actionview (= 4.1.2) + actionmailer (4.1.4) + actionpack (= 4.1.4) + actionview (= 4.1.4) mail (~> 2.5.4) - actionpack (4.1.2) - actionview (= 4.1.2) - activesupport (= 4.1.2) + actionpack (4.1.4) + actionview (= 4.1.4) + activesupport (= 4.1.4) rack (~> 1.5.2) rack-test (~> 0.6.2) - actionview (4.1.2) - activesupport (= 4.1.2) + actionview (4.1.4) + activesupport (= 4.1.4) builder (~> 3.1) erubis (~> 2.7.0) active_model_serializers (0.8.1) activemodel (>= 3.0) - activemodel (4.1.2) - activesupport (= 4.1.2) + activemodel (4.1.4) + activesupport (= 4.1.4) builder (~> 3.1) - activerecord (4.1.2) - activemodel (= 4.1.2) - activesupport (= 4.1.2) + activerecord (4.1.4) + activemodel (= 4.1.4) + activesupport (= 4.1.4) arel (~> 5.0.0) - activesupport (4.1.2) + activesupport (4.1.4) i18n (~> 0.6, >= 0.6.9) json (~> 1.7, >= 1.7.7) minitest (~> 5.1) @@ -154,24 +154,24 @@ GEM rack-test (0.6.2) rack (>= 1.0) rack-timeout (0.0.4) - rails (4.1.2) - actionmailer (= 4.1.2) - actionpack (= 4.1.2) - actionview (= 4.1.2) - activemodel (= 4.1.2) - activerecord (= 4.1.2) - activesupport (= 4.1.2) + rails (4.1.4) + actionmailer (= 4.1.4) + actionpack (= 4.1.4) + actionview (= 4.1.4) + activemodel (= 4.1.4) + activerecord (= 4.1.4) + activesupport (= 4.1.4) bundler (>= 1.3.0, < 2.0) - railties (= 4.1.2) + railties (= 4.1.4) sprockets-rails (~> 2.0) rails_12factor (0.0.2) rails_serve_static_assets rails_stdout_logging rails_serve_static_assets (0.0.1) rails_stdout_logging (0.0.2) - railties (4.1.2) - actionpack (= 4.1.2) - activesupport (= 4.1.2) + railties (4.1.4) + actionpack (= 4.1.4) + activesupport (= 4.1.4) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rainbow (2.0.0) From f63ead8fa23902f0b88417678ed3589c61a94c0f Mon Sep 17 00:00:00 2001 From: Moncef Belyamani Date: Thu, 3 Jul 2014 23:31:56 -0400 Subject: [PATCH 08/17] Update Spring to version 1.1.3 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 4ff701684..3a8bdcf76 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -220,7 +220,7 @@ GEM simplecov-html (~> 0.8.0) simplecov-html (0.8.0) slop (3.5.0) - spring (1.1.2) + spring (1.1.3) spring-commands-rspec (1.0.1) spring (>= 0.9.1) sprockets (2.11.0) From fa3c26729ea8e172e2dc556bf965647a23228688 Mon Sep 17 00:00:00 2001 From: Moncef Belyamani Date: Thu, 3 Jul 2014 23:35:34 -0400 Subject: [PATCH 09/17] Update spring-commands-rspec to version 1.0.2 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 3a8bdcf76..312015a11 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -221,7 +221,7 @@ GEM simplecov-html (0.8.0) slop (3.5.0) spring (1.1.3) - spring-commands-rspec (1.0.1) + spring-commands-rspec (1.0.2) spring (>= 0.9.1) sprockets (2.11.0) hike (~> 1.2) From f87b79a2483701430417e681a64bebf17177419a Mon Sep 17 00:00:00 2001 From: Moncef Belyamani Date: Thu, 3 Jul 2014 23:35:59 -0400 Subject: [PATCH 10/17] Update Capybara to version 2.4.1 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 312015a11..93a70083b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -47,7 +47,7 @@ GEM bullet (4.11.3) activesupport (>= 3.0.0) uniform_notifier (>= 1.6.0) - capybara (2.3.0) + capybara (2.4.1) mime-types (>= 1.16) nokogiri (>= 1.3.3) rack (>= 1.0.0) From 924709cbf2bcbb934f598df82beb32c958d75102 Mon Sep 17 00:00:00 2001 From: Moncef Belyamani Date: Sun, 6 Jul 2014 21:08:25 -0400 Subject: [PATCH 11/17] Refactor code to improve Code Climate grade. --- app/controllers/api/v1/search_controller.rb | 21 ++++++-------- app/decorators/admin/admin_decorator.rb | 28 ++++++++----------- app/models/concerns/search.rb | 8 +++--- app/models/location.rb | 6 ++-- .../initializers/filter_parameter_logging.rb | 2 +- spec/api/patch_address_spec.rb | 2 +- 6 files changed, 29 insertions(+), 38 deletions(-) diff --git a/app/controllers/api/v1/search_controller.rb b/app/controllers/api/v1/search_controller.rb index 1a260068f..2397b4577 100644 --- a/app/controllers/api/v1/search_controller.rb +++ b/app/controllers/api/v1/search_controller.rb @@ -15,19 +15,16 @@ def index def nearby location = Location.find(params[:location_id]) - if params[:radius].present? - radius = Location.validated_radius(params[:radius]) - else - radius = 0.5 - end + radius = Location.validated_radius(params[:radius], 0.5) - if location.latitude.present? && location.longitude.present? - nearby = location.nearbys(radius). - page(params[:page]).per(params[:per_page]). - includes(:organization, :address, :phones) - else - nearby = Location.none.page(params[:page]).per(params[:per_page]) - end + nearby = + if location.coordinates.present? + location.nearbys(radius). + page(params[:page]).per(params[:per_page]). + includes(:organization, :address, :phones) + else + Location.none.page(params[:page]).per(params[:per_page]) + end render json: nearby, status: 200 generate_pagination_headers(nearby) diff --git a/app/decorators/admin/admin_decorator.rb b/app/decorators/admin/admin_decorator.rb index f2561e032..ffc6e6820 100644 --- a/app/decorators/admin/admin_decorator.rb +++ b/app/decorators/admin/admin_decorator.rb @@ -7,7 +7,7 @@ def initialize(admin) end def allowed_to_access_location?(location) - return true if location_admins_match_admin_email?(location) || admin.super_admin? + return true if location_admin_emails_match_admin_email?(location) || admin.super_admin? if admin_has_generic_email? location_emails_match_admin_email?(location) else @@ -19,24 +19,18 @@ def domain admin.email.split('@').last end - def location_urls_match_domain?(location) - return false unless location.urls.present? - location.urls.select { |url| url.include?(domain) }.length > 0 - end - - def location_emails_match_domain?(location) - return false unless location.emails.present? - location.emails.select { |email| email.include?(domain) }.length > 0 - end - - def location_emails_match_admin_email?(location) - return false unless location.emails.present? - location.emails.include?(admin.email) + %w(urls emails).each do |name| + define_method "location_#{name}_match_domain?" do |location| + return false unless location.send(name).present? + location.send(name).select { |attr| attr.include?(domain) }.present? + end end - def location_admins_match_admin_email?(location) - return false unless location.admin_emails.present? - location.admin_emails.include?(admin.email) + %w(admin_emails emails).each do |name| + define_method "location_#{name}_match_admin_email?" do |location| + return false unless location.send(name).present? + location.send(name).include?(admin.email) + end end def admin_has_generic_email? diff --git a/app/models/concerns/search.rb b/app/models/concerns/search.rb index a0c027189..63679f526 100644 --- a/app/models/concerns/search.rb +++ b/app/models/concerns/search.rb @@ -10,10 +10,10 @@ module Search if loc.present? result = Geocoder.search(loc, bounds: SETTINGS[:bounds]) coords = result.first.coordinates if result.present? - near(coords, validated_radius(r)) + near(coords, validated_radius(r, 5)) elsif lat_lng.present? coords = validated_coordinates(lat_lng) - near(coords, validated_radius(r)) + near(coords, validated_radius(r, 5)) end end) @@ -62,8 +62,8 @@ def text_search(params = {}) has_keyword(params[:keyword]) end - def validated_radius(radius) - return 5 unless radius.present? + def validated_radius(radius, custom_radius) + return custom_radius unless radius.present? if radius.to_f == 0.0 fail Exceptions::InvalidRadius else diff --git a/app/models/location.rb b/app/models/location.rb index 0e3cd6f63..671ec0adf 100644 --- a/app/models/location.rb +++ b/app/models/location.rb @@ -112,9 +112,9 @@ class Location < ActiveRecord::Base before_save :compact_array_fields def compact_array_fields - admin_emails.reject!(&:blank?) if admin_emails.is_a?(Array) - emails.reject!(&:blank?) if emails.is_a?(Array) - urls.reject!(&:blank?) if urls.is_a?(Array) + %w(admin_emails emails urls).each do |name| + send("#{name}=", send(name).reject(&:blank?)) if send(name).is_a?(Array) + end end extend FriendlyId diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb index 2ab23e63c..028e9bb81 100644 --- a/config/initializers/filter_parameter_logging.rb +++ b/config/initializers/filter_parameter_logging.rb @@ -1,4 +1,4 @@ # Be sure to restart your server when you modify this file. # Configure sensitive parameters which will be filtered from the log file. -Rails.application.config.filter_parameters += [:password, :password_confirmation] +Rails.application.config.filter_parameters += [:password, :password_confirmation, :api_token] diff --git a/spec/api/patch_address_spec.rb b/spec/api/patch_address_spec.rb index bc0958c51..4947b4f6a 100644 --- a/spec/api/patch_address_spec.rb +++ b/spec/api/patch_address_spec.rb @@ -50,7 +50,7 @@ it 'requires a valid address id' do patch( - api_location_address_url(@loc, 123, subdomain: ENV['API_SUBDOMAIN']), + api_location_address_url(@loc, 1234, subdomain: ENV['API_SUBDOMAIN']), @attrs ) expect(response.status).to eq(404) From b1418ff2a83175d261cfe1273b6fb4960b8843f1 Mon Sep 17 00:00:00 2001 From: Moncef Belyamani Date: Mon, 7 Jul 2014 23:45:49 -0400 Subject: [PATCH 12/17] Add more specs for location attributes - Fix HTML and JS issues with form elements: wrap each array field or set of association fields in a fieldset, add a unique ID to each new array field, add CSS styles for buttons that add fields - Reject all_blank attributes for faxes - Add specs for faxes and admin_emails - Update specs for contacts --- app/assets/javascripts/form.js.coffee | 7 + app/assets/stylesheets/application.css.scss | 9 +- app/decorators/admin/admin_decorator.rb | 2 - app/helpers/admin/form_helper.rb | 2 +- app/models/location.rb | 3 +- .../admin/locations/forms/_address.html.haml | 12 +- .../locations/forms/_address_fields.html.haml | 28 ++-- .../forms/_admin_email_fields.html.haml | 6 +- .../locations/forms/_admin_emails.html.haml | 11 +- .../locations/forms/_contact_fields.html.haml | 40 +++--- .../admin/locations/forms/_contacts.html.haml | 11 +- .../locations/forms/_email_fields.html.haml | 6 +- .../admin/locations/forms/_emails.html.haml | 11 +- .../locations/forms/_fax_fields.html.haml | 17 ++- .../admin/locations/forms/_faxes.html.haml | 11 +- .../locations/forms/_mail_address.html.haml | 12 +- .../forms/_mail_address_fields.html.haml | 20 ++- .../locations/forms/_phone_fields.html.haml | 28 ++-- .../admin/locations/forms/_phones.html.haml | 11 +- .../locations/forms/_url_fields.html.haml | 6 +- .../admin/locations/forms/_urls.html.haml | 15 +-- .../admin/locations/update_contacts_spec.rb | 34 +++-- .../locations/update_fax_numbers_spec.rb | 121 ++++++++++++++++++ .../locations/update_location_admins_spec.rb | 50 ++++++++ spec/support/features/form_helpers.rb | 34 ++++- 25 files changed, 376 insertions(+), 131 deletions(-) create mode 100644 spec/features/admin/locations/update_fax_numbers_spec.rb create mode 100644 spec/features/admin/locations/update_location_admins_spec.rb diff --git a/app/assets/javascripts/form.js.coffee b/app/assets/javascripts/form.js.coffee index 4f9831e23..aac55b85a 100644 --- a/app/assets/javascripts/form.js.coffee +++ b/app/assets/javascripts/form.js.coffee @@ -15,6 +15,13 @@ jQuery -> $(this).before($(this).data('fields').replace(regexp, time)) event.preventDefault() + $('.edit_location').on 'click', '.add_array_fields', (event) -> + time = new Date().getTime() + $(this).before($(this).data('fields')) + inputs = $(this).parent().find('input[type=text]') + inputs[inputs.length - 1].setAttribute('id', time) + event.preventDefault() + $('.new_location').on 'click', '.add_fields', (event) -> time = new Date().getTime() regexp = new RegExp($(this).data('id'), 'g') diff --git a/app/assets/stylesheets/application.css.scss b/app/assets/stylesheets/application.css.scss index a2bf6ecee..5e35e4f43 100644 --- a/app/assets/stylesheets/application.css.scss +++ b/app/assets/stylesheets/application.css.scss @@ -86,14 +86,19 @@ p { - padding:10px; + padding:0px 0px 10px 10px; } fieldset { - padding:10px; + padding:10px 0px 10px 10px; } } +.add_fields, .add_array_fields +{ + margin: 10px 0px 15px 10px; +} + #categories, #accessibility { ul {list-style-type: none; margin-left:15px;} diff --git a/app/decorators/admin/admin_decorator.rb b/app/decorators/admin/admin_decorator.rb index ffc6e6820..6f7a485e1 100644 --- a/app/decorators/admin/admin_decorator.rb +++ b/app/decorators/admin/admin_decorator.rb @@ -21,14 +21,12 @@ def domain %w(urls emails).each do |name| define_method "location_#{name}_match_domain?" do |location| - return false unless location.send(name).present? location.send(name).select { |attr| attr.include?(domain) }.present? end end %w(admin_emails emails).each do |name| define_method "location_#{name}_match_admin_email?" do |location| - return false unless location.send(name).present? location.send(name).include?(admin.email) end end diff --git a/app/helpers/admin/form_helper.rb b/app/helpers/admin/form_helper.rb index 18beade1f..2f4c26cc8 100644 --- a/app/helpers/admin/form_helper.rb +++ b/app/helpers/admin/form_helper.rb @@ -12,7 +12,7 @@ def link_to_add_fields(name, f, association) def link_to_add_array_fields(name, field) id = ''.object_id fields = render("admin/locations/forms/#{field}_fields") - link_to(name, '#', class: 'add_fields btn btn-primary', data: { id: id, fields: fields.gsub('\n', '') }) + link_to(name, '#', class: 'add_array_fields btn btn-primary', data: { id: id, fields: fields.gsub('\n', '') }) end end end diff --git a/app/models/location.rb b/app/models/location.rb index 671ec0adf..6a77b6696 100644 --- a/app/models/location.rb +++ b/app/models/location.rb @@ -18,7 +18,8 @@ class Location < ActiveRecord::Base allow_destroy: true, reject_if: :all_blank has_many :faxes, dependent: :destroy - accepts_nested_attributes_for :faxes, allow_destroy: true + accepts_nested_attributes_for :faxes, + allow_destroy: true, reject_if: :all_blank has_one :mail_address, dependent: :destroy accepts_nested_attributes_for :mail_address, allow_destroy: true diff --git a/app/views/admin/locations/forms/_address.html.haml b/app/views/admin/locations/forms/_address.html.haml index 75fc0d9e6..ba9a8f8ca 100644 --- a/app/views/admin/locations/forms/_address.html.haml +++ b/app/views/admin/locations/forms/_address.html.haml @@ -1,12 +1,10 @@ -%div.inst-box +%div.inst-box.address %header %strong Street Address %span.desc The physical location. - = field_set_tag nil, id: 'address' do - = f.fields_for :address do |builder| - = render 'admin/locations/forms/address_fields', f: builder - = link_to 'Delete this address permanently', '#', class: 'btn btn-danger delete_association' - - unless @location.address.present? - = link_to_add_fields 'Add a street address', f, :address + = f.fields_for :address do |builder| + = render 'admin/locations/forms/address_fields', f: builder + - unless @location.address.present? + = link_to_add_fields 'Add a street address', f, :address diff --git a/app/views/admin/locations/forms/_address_fields.html.haml b/app/views/admin/locations/forms/_address_fields.html.haml index 7fc87f5ac..e96e64755 100644 --- a/app/views/admin/locations/forms/_address_fields.html.haml +++ b/app/views/admin/locations/forms/_address_fields.html.haml @@ -1,13 +1,15 @@ -= f.label :street, 'Street' -= f.text_field :street -%br -= f.label :city, 'City' -= f.text_field :city, maxlength: 255 -%br -= f.label :state, 'State (2-letter abbreviation)' -= f.text_field :state, maxlength: 2, class: 'span1' -%br -= f.label :zip, 'ZIP Code' -= f.text_field :zip, maxlength: 5, class: 'span2' -= f.hidden_field :_destroy -%br += field_set_tag do + = f.label :street, 'Street' + = f.text_field :street + %br + = f.label :city, 'City' + = f.text_field :city, maxlength: 255 + %br + = f.label :state, 'State (2-letter abbreviation)' + = f.text_field :state, maxlength: 2, class: 'span1' + %br + = f.label :zip, 'ZIP Code' + = f.text_field :zip, maxlength: 5, class: 'span2' + = f.hidden_field :_destroy + %br + = link_to 'Delete this address permanently', '#', class: 'btn btn-danger delete_association' diff --git a/app/views/admin/locations/forms/_admin_email_fields.html.haml b/app/views/admin/locations/forms/_admin_email_fields.html.haml index c6a6fce73..a8e3e9c7d 100644 --- a/app/views/admin/locations/forms/_admin_email_fields.html.haml +++ b/app/views/admin/locations/forms/_admin_email_fields.html.haml @@ -1,2 +1,4 @@ -= text_field_tag 'location[admin_emails][]', '', class: 'span4' -%br \ No newline at end of file += field_set_tag do + = text_field_tag 'location[admin_emails][]', '', class: 'span4' + %br + = link_to "Delete this admin permanently", '#', class: "btn btn-danger delete_attribute" \ No newline at end of file diff --git a/app/views/admin/locations/forms/_admin_emails.html.haml b/app/views/admin/locations/forms/_admin_emails.html.haml index a153b5dce..d16a99408 100644 --- a/app/views/admin/locations/forms/_admin_emails.html.haml +++ b/app/views/admin/locations/forms/_admin_emails.html.haml @@ -1,15 +1,14 @@ - if @admin_decorator.allowed_to_access_location?(@location) - %div.inst-box + %div.inst-box.admin_emails %header %strong Add an admin to this location %p.desc Which email addresses should be allowed to update and delete this location? - = field_set_tag nil, id: 'admin_emails' do - - if @location.admin_emails.present? - - @location.admin_emails.each_with_index do |admin, i| + - if @location.admin_emails.present? + - @location.admin_emails.each_with_index do |admin, i| + = field_set_tag do = text_field_tag 'location[admin_emails][]', admin, class: 'span4', id: "location_admin_emails_#{i}" %br = link_to "Delete this admin permanently", '#', class: "btn btn-danger delete_attribute" - %p - = link_to_add_array_fields 'Add an admin email', :admin_email + = link_to_add_array_fields 'Add an admin email', :admin_email diff --git a/app/views/admin/locations/forms/_contact_fields.html.haml b/app/views/admin/locations/forms/_contact_fields.html.haml index 73e801325..820cd1ca9 100644 --- a/app/views/admin/locations/forms/_contact_fields.html.haml +++ b/app/views/admin/locations/forms/_contact_fields.html.haml @@ -1,20 +1,22 @@ -= f.label :name, 'Name' -= f.text_field :name, maxlength: 255, class: 'span5' -%br -= f.label :title, 'Title' -= f.text_field :title, maxlength: 255, class: 'span5' -%br -= f.label :email, 'Email' -= f.text_field :email, maxlength: 255, class: 'span5' -%br -= f.label :phone, 'Phone' -= f.text_field :phone, maxlength: 12, class: 'span2' -%br -= f.label :extension, 'Extension' -= f.text_field :extension, maxlength: 12, class: 'span2' -%br -= f.label :fax, 'Fax' -= f.text_field :fax, maxlength: 12, class: 'span2' += field_set_tag do + = f.label :name, 'Name' + = f.text_field :name, maxlength: 255, class: 'span5' + %br + = f.label :title, 'Title' + = f.text_field :title, maxlength: 255, class: 'span5' + %br + = f.label :email, 'Email' + = f.text_field :email, maxlength: 255, class: 'span5' + %br + = f.label :phone, 'Phone' + = f.text_field :phone, maxlength: 12, class: 'span2' + %br + = f.label :extension, 'Extension' + = f.text_field :extension, maxlength: 12, class: 'span2' + %br + = f.label :fax, 'Fax' + = f.text_field :fax, maxlength: 12, class: 'span2' -= f.hidden_field :_destroy -%br \ No newline at end of file + = f.hidden_field :_destroy + %br + = link_to "Delete this contact permanently", '#', class: "btn btn-danger delete_association" diff --git a/app/views/admin/locations/forms/_contacts.html.haml b/app/views/admin/locations/forms/_contacts.html.haml index d88f4e423..9361ce00b 100644 --- a/app/views/admin/locations/forms/_contacts.html.haml +++ b/app/views/admin/locations/forms/_contacts.html.haml @@ -1,12 +1,9 @@ -%div.inst-box +%div.inst-box.contacts %header %strong Contacts %p.desc Who are the main points of contact at the location? - = field_set_tag nil, id: 'contacts' do - = f.fields_for :contacts do |builder| - = render 'admin/locations/forms/contact_fields', f: builder - = link_to "Delete this contact permanently", '#', class: "btn btn-danger delete_association" - %p - = link_to_add_fields 'Add a contact', f, :contacts \ No newline at end of file + = f.fields_for :contacts do |builder| + = render 'admin/locations/forms/contact_fields', f: builder + = link_to_add_fields 'Add a contact', f, :contacts \ No newline at end of file diff --git a/app/views/admin/locations/forms/_email_fields.html.haml b/app/views/admin/locations/forms/_email_fields.html.haml index 913314e68..e7d49e93c 100644 --- a/app/views/admin/locations/forms/_email_fields.html.haml +++ b/app/views/admin/locations/forms/_email_fields.html.haml @@ -1,2 +1,4 @@ -= email_field_tag 'location[emails][]', '', class: 'span5' -%br += field_set_tag do + = email_field_tag 'location[emails][]', '', class: 'span5' + %br + = link_to "Delete this email permanently", '#', class: "btn btn-danger delete_attribute" diff --git a/app/views/admin/locations/forms/_emails.html.haml b/app/views/admin/locations/forms/_emails.html.haml index 1986d79fe..a55974765 100644 --- a/app/views/admin/locations/forms/_emails.html.haml +++ b/app/views/admin/locations/forms/_emails.html.haml @@ -1,15 +1,14 @@ -%div.inst-box +%div.inst-box.emails %header %strong Emails (general info) %p.desc %em If the email belongs to a contact, please move it to the existing contact, or create a new contact. - = field_set_tag nil, id: 'emails' do - - if @location.emails.present? - - @location.emails.each_with_index do |email, i| + - if @location.emails.present? + - @location.emails.each_with_index do |email, i| + = field_set_tag do = email_field_tag 'location[emails][]', email, class: 'span6', id: "location_emails_#{i}" %br = link_to "Delete this email permanently", '#', class: "btn btn-danger delete_attribute" - %p - = link_to_add_array_fields 'Add a general email', :email + = link_to_add_array_fields 'Add a general email', :email diff --git a/app/views/admin/locations/forms/_fax_fields.html.haml b/app/views/admin/locations/forms/_fax_fields.html.haml index 0c11b7e58..2692915cc 100644 --- a/app/views/admin/locations/forms/_fax_fields.html.haml +++ b/app/views/admin/locations/forms/_fax_fields.html.haml @@ -1,8 +1,11 @@ -= f.label :name, 'Number' -= f.text_field :number, maxlength: 12, class: 'span2' -%br -= f.label :department, 'Department' -= f.text_field :department, maxlength: 50, class: 'span4' += field_set_tag do + = f.label :name, 'Number' + = f.text_field :number, maxlength: 12, class: 'span2', autocomplete: true + %br + = f.label :department, 'Department' + = f.text_field :department, maxlength: 50, class: 'span4' + + = f.hidden_field :_destroy + %br + = link_to 'Delete this fax permanently', '#', class: 'btn btn-danger delete_association' -= f.hidden_field :_destroy -%br \ No newline at end of file diff --git a/app/views/admin/locations/forms/_faxes.html.haml b/app/views/admin/locations/forms/_faxes.html.haml index 3ddf8432b..096ad81cd 100644 --- a/app/views/admin/locations/forms/_faxes.html.haml +++ b/app/views/admin/locations/forms/_faxes.html.haml @@ -1,4 +1,4 @@ -%div.inst-box +%div.inst-box.faxes %header %strong Fax Numbers @@ -7,9 +7,6 @@ 10 digits only please, in this format: 650-802-7922. %br If the fax number belongs to a contact, please move it to the existing contact, or create a new contact. - = field_set_tag nil, id: 'faxes' do - = f.fields_for :faxes do |builder| - = render 'admin/locations/forms/fax_fields', f: builder - = link_to "Delete this fax permanently", '#', class: "btn btn-danger delete_association" - %p - = link_to_add_fields 'Add a fax number', f, :faxes + = f.fields_for :faxes do |builder| + = render 'admin/locations/forms/fax_fields', f: builder + = link_to_add_fields 'Add a fax number', f, :faxes diff --git a/app/views/admin/locations/forms/_mail_address.html.haml b/app/views/admin/locations/forms/_mail_address.html.haml index c52539e29..ebcecc343 100644 --- a/app/views/admin/locations/forms/_mail_address.html.haml +++ b/app/views/admin/locations/forms/_mail_address.html.haml @@ -1,10 +1,8 @@ -%div.inst-box +%div.inst-box.mail_address %header %strong Mailing Address - = field_set_tag nil, id: 'mail_address' do - = f.fields_for :mail_address do |builder| - = render 'admin/locations/forms/mail_address_fields', f: builder - = link_to 'Delete this mailing address permanently', '#', class: 'btn btn-danger delete_association' - - unless @location.mail_address.present? - = link_to_add_fields 'Add a mailing address', f, :mail_address + = f.fields_for :mail_address do |builder| + = render 'admin/locations/forms/mail_address_fields', f: builder + - unless @location.mail_address.present? + = link_to_add_fields 'Add a mailing address', f, :mail_address diff --git a/app/views/admin/locations/forms/_mail_address_fields.html.haml b/app/views/admin/locations/forms/_mail_address_fields.html.haml index 35f3e3f14..77df61ca3 100644 --- a/app/views/admin/locations/forms/_mail_address_fields.html.haml +++ b/app/views/admin/locations/forms/_mail_address_fields.html.haml @@ -1,3 +1,17 @@ -= f.label :attention, 'Attention' -= f.text_field :attention, maxlength: 255, class: 'span5' -= render 'admin/locations/forms/address_fields', f: f \ No newline at end of file += field_set_tag do + = f.label :attention, 'Attention' + = f.text_field :attention, maxlength: 255, class: 'span5' + = f.label :street, 'Street' + = f.text_field :street + %br + = f.label :city, 'City' + = f.text_field :city, maxlength: 255 + %br + = f.label :state, 'State (2-letter abbreviation)' + = f.text_field :state, maxlength: 2, class: 'span1' + %br + = f.label :zip, 'ZIP Code' + = f.text_field :zip, maxlength: 5, class: 'span2' + = f.hidden_field :_destroy + %br + = link_to 'Delete this mailing address permanently', '#', class: 'btn btn-danger delete_association' diff --git a/app/views/admin/locations/forms/_phone_fields.html.haml b/app/views/admin/locations/forms/_phone_fields.html.haml index 59d060109..9adcbe0ac 100644 --- a/app/views/admin/locations/forms/_phone_fields.html.haml +++ b/app/views/admin/locations/forms/_phone_fields.html.haml @@ -1,14 +1,16 @@ -= f.label :number, 'Number' -= f.text_field :number, maxlength: 12, class: 'span2' -%br -= f.label :vanity_number, 'Vanity Number (for example: 650-123-HELP)' -= f.text_field :vanity_number, maxlength: 12, class: 'span2' -%br -= f.label :extension, 'Extension' -= f.text_field :extension, maxlength: 8, class: 'span2' -%br -= f.label :department, 'Department' -= f.text_field :department, maxlength: 50, class: 'span4' += field_set_tag do + = f.label :number, 'Number' + = f.text_field :number, maxlength: 12, class: 'span2' + %br + = f.label :vanity_number, 'Vanity Number (for example: 650-123-HELP)' + = f.text_field :vanity_number, maxlength: 12, class: 'span2' + %br + = f.label :extension, 'Extension' + = f.text_field :extension, maxlength: 8, class: 'span2' + %br + = f.label :department, 'Department' + = f.text_field :department, maxlength: 50, class: 'span4' -= f.hidden_field :_destroy -%br \ No newline at end of file + = f.hidden_field :_destroy + %br + = link_to "Delete this phone permanently", '#', class: "btn btn-danger delete_association" \ No newline at end of file diff --git a/app/views/admin/locations/forms/_phones.html.haml b/app/views/admin/locations/forms/_phones.html.haml index 50312cd6b..f81bd1f0b 100644 --- a/app/views/admin/locations/forms/_phones.html.haml +++ b/app/views/admin/locations/forms/_phones.html.haml @@ -1,4 +1,4 @@ -%div.inst-box +%div.inst-box.phones %header %strong Phone Numbers @@ -7,9 +7,6 @@ 10 digits only please, in this format: 650-802-7922. %br If the phone number belongs to a contact, please move it to the existing contact, or create a new contact. - = field_set_tag nil, id: 'phones' do - = f.fields_for :phones do |builder| - = render 'admin/locations/forms/phone_fields', f: builder - = link_to "Delete this phone permanently", '#', class: "btn btn-danger delete_association" - %p - = link_to_add_fields 'Add a phone number', f, :phones + = f.fields_for :phones do |builder| + = render 'admin/locations/forms/phone_fields', f: builder + = link_to_add_fields 'Add a phone number', f, :phones diff --git a/app/views/admin/locations/forms/_url_fields.html.haml b/app/views/admin/locations/forms/_url_fields.html.haml index 33f6b2c02..bc6e808a0 100644 --- a/app/views/admin/locations/forms/_url_fields.html.haml +++ b/app/views/admin/locations/forms/_url_fields.html.haml @@ -1,2 +1,4 @@ -= url_field_tag 'location[urls][]', '', class: 'span9' -%br += field_set_tag do + = url_field_tag 'location[urls][]', '', class: 'span9' + %br + = link_to "Delete this website permanently", '#', class: "btn btn-danger delete_attribute" diff --git a/app/views/admin/locations/forms/_urls.html.haml b/app/views/admin/locations/forms/_urls.html.haml index 0c4af78c8..22375a4a3 100644 --- a/app/views/admin/locations/forms/_urls.html.haml +++ b/app/views/admin/locations/forms/_urls.html.haml @@ -1,4 +1,4 @@ -%div.inst-box +%div.inst-box.urls %header %strong Websites @@ -6,14 +6,13 @@ What websites are associated with the location? %em Must include "http://" or "https://" - = field_set_tag nil, id: 'urls' do - - if f.object.new_record? - = url_field_tag "location[urls][]", @location_url, class: 'span9' - - elsif @location.urls.present? - - @location.urls.each_with_index do |url, i| + - if f.object.new_record? + = url_field_tag "location[urls][]", @location_url, class: 'span9' + - elsif @location.urls.present? + - @location.urls.each_with_index do |url, i| + = field_set_tag do = url_field_tag 'location[urls][]', url, class: 'span9', id: "location_urls_#{i}" %br = link_to "Delete this website permanently", '#', class: "btn btn-danger delete_attribute" - %p - = link_to_add_array_fields 'Add a website', :url + = link_to_add_array_fields 'Add a website', :url diff --git a/spec/features/admin/locations/update_contacts_spec.rb b/spec/features/admin/locations/update_contacts_spec.rb index 69e7c34e8..280c9d693 100644 --- a/spec/features/admin/locations/update_contacts_spec.rb +++ b/spec/features/admin/locations/update_contacts_spec.rb @@ -2,16 +2,16 @@ feature 'Update contacts' do background do - create(:location) + @location = create(:location) login_super_admin visit '/admin/locations/vrs-services' end scenario 'when no contacts exist' do - within('#contacts') do - expect(page). - to have_no_xpath('.//input[contains(@name, "[name]")]') - end + expect(page). + to have_no_xpath('//input[@id="location_contacts_attributes_0_name"]') + + expect(page).to_not have_link 'Delete this contact permanently' end scenario 'by adding a new contact', :js do @@ -45,7 +45,7 @@ delete_contact visit '/admin/locations/vrs-services' - within('#contacts') do + within('.contacts') do expect(page). to have_no_xpath('.//input[contains(@name, "[name]")]') end @@ -60,7 +60,7 @@ click_button 'Save changes' visit '/admin/locations/vrs-services' - within('#contacts') do + within('.contacts') do total_contacts = all(:xpath, './/input[contains(@name, "[name]")]') expect(total_contacts.length).to eq 1 end @@ -72,13 +72,31 @@ title: 'Director of Development and Operations' ) click_link 'Add a contact' - within('#contacts') do + within('.contacts') do all_phones = all(:xpath, './/input[contains(@name, "[phone]")]') fill_in all_phones[-1][:id], with: '202-555-1212' end click_button 'Save changes' expect(page).to have_content "name can't be blank for Contact" end + + scenario 'delete second contact', :js do + @location.contacts.create!(name: 'foo', title: 'bar') + new_loc = create(:nearby_loc) + new_loc.contacts.create!(name: 'bar', title: 'foo') + new_loc.contacts.create!(name: 'baz', title: 'boo') + + visit '/admin/locations/library' + + find(:xpath, "(//a[text()='Delete this contact permanently'])[2]").click + click_button 'Save changes' + + expect(find_field('location_contacts_attributes_0_name').value). + to eq 'bar' + + expect(page). + to have_no_xpath('//input[@id="location_contacts_attributes_1_name"]') + end end feature 'Update contacts' do diff --git a/spec/features/admin/locations/update_fax_numbers_spec.rb b/spec/features/admin/locations/update_fax_numbers_spec.rb new file mode 100644 index 000000000..d350e8d68 --- /dev/null +++ b/spec/features/admin/locations/update_fax_numbers_spec.rb @@ -0,0 +1,121 @@ +require 'rails_helper' + +feature 'Update faxes' do + background do + @location = create(:location) + login_super_admin + visit '/admin/locations/vrs-services' + end + + scenario 'when no faxes exist' do + within('.faxes') do + expect(page). + to have_no_xpath('.//input[contains(@name, "[number]")]') + end + + expect(page).to_not have_link 'Delete this fax permanently' + end + + scenario 'by adding a new fax', :js do + add_fax( + number: '123-456-7890', + department: 'Director of Development' + ) + click_button 'Save changes' + visit '/admin/locations/vrs-services' + + expect(find_field('location_faxes_attributes_0_number').value). + to eq '123-456-7890' + + expect(find_field('location_faxes_attributes_0_department').value). + to eq 'Director of Development' + + delete_fax + visit '/admin/locations/vrs-services' + within('.faxes') do + expect(page). + to have_no_xpath('.//input[contains(@name, "[department]")]') + end + end + + scenario 'with 2 faxes but one is empty', :js do + add_fax( + number: '123-456-7890', + department: 'Director of Development' + ) + click_link 'Add a fax number' + click_button 'Save changes' + visit '/admin/locations/vrs-services' + + within('.faxes') do + total_faxes = all(:xpath, './/input[contains(@name, "[number]")]') + expect(total_faxes.length).to eq 1 + end + end + + scenario 'with 2 faxes but second one is invalid', :js do + add_fax( + number: '123-456-7890', + department: 'Director of Development' + ) + click_link 'Add a fax' + within('.faxes') do + all_faxes = all(:xpath, './/input[contains(@name, "[department]")]') + fill_in all_faxes[-1][:id], with: 'Department' + end + click_button 'Save changes' + expect(page).to have_content "number can't be blank for Fax" + end + + scenario 'delete second fax', :js do + # There was a bug where clicking the "Delete this fax permanently" + # button would hide all faxes from the form, and would set the id + # of the first fax to "1", causing an error when submitting the form. + # To test this, we need to make sure neither of the faxes we are trying + # to delete have an id of 1, so we need to create 3 faxes first and + # test with the last 2. + @location.faxes.create!(number: '124-456-7890', department: 'Ruby') + new_loc = create(:nearby_loc) + new_loc.faxes.create!(number: '123-456-7890', department: 'Python') + new_loc.faxes.create!(number: '456-123-7890', department: 'JS') + + visit '/admin/locations/library' + + find(:xpath, "(//a[text()='Delete this fax permanently'])[2]").click + click_button 'Save changes' + + expect(find_field('location_faxes_attributes_0_number').value). + to eq '123-456-7890' + + expect(page). + to have_no_xpath('//input[@id="location_faxes_attributes_1_number"]') + end +end + +feature 'Update faxes' do + before(:all) do + @location = create(:location) + @location.faxes.create!(number: '123-456-7890', department: 'Ruby') + end + + before(:each) do + login_super_admin + visit '/admin/locations/vrs-services' + end + + after(:all) do + Organization.find_each(&:destroy) + end + + scenario 'with an empty number' do + update_fax(number: '') + click_button 'Save changes' + expect(page).to have_content "number can't be blank for Fax" + end + + scenario 'with an invalid number' do + update_fax(number: '703') + click_button 'Save changes' + expect(page).to have_content 'is not a valid US fax number' + end +end diff --git a/spec/features/admin/locations/update_location_admins_spec.rb b/spec/features/admin/locations/update_location_admins_spec.rb new file mode 100644 index 000000000..fa9248c75 --- /dev/null +++ b/spec/features/admin/locations/update_location_admins_spec.rb @@ -0,0 +1,50 @@ +require 'rails_helper' + +feature 'Update admin_emails' do + background do + create(:location) + login_super_admin + visit '/admin/locations/vrs-services' + end + + scenario 'when no admin_emails exist' do + expect(page).to have_no_xpath("//input[@name='location[admin_emails][]']") + expect(page).to_not have_link 'Delete this admin permanently' + end + + scenario 'by adding 2 new admins', :js do + add_two_admins + visit '/admin/locations/vrs-services' + total_admins = page. + all(:xpath, "//input[@name='location[admin_emails][]']") + expect(total_admins.length).to eq 2 + delete_all_admins + visit '/admin/locations/vrs-services' + expect(page).to have_no_xpath("//input[@name='location[admin_emails][]']") + end + + scenario 'with empty admin', :js do + click_link 'Add an admin' + click_button 'Save changes' + visit '/admin/locations/vrs-services' + expect(page).to have_no_xpath("//input[@name='location[admin_emails][]']") + end + + scenario 'with 2 admins but one is empty', :js do + click_link 'Add an admin' + fill_in 'location[admin_emails][]', with: 'moncef@samaritanhouse.com' + click_link 'Add an admin' + click_button 'Save changes' + visit '/admin/locations/vrs-services' + total_admins = all(:xpath, "//input[@name='location[admin_emails][]']") + expect(total_admins.length).to eq 1 + end + + scenario 'with invalid admin', :js do + click_link 'Add an admin' + fill_in 'location[admin_emails][]', with: 'moncefsamaritanhouse.com' + click_button 'Save changes' + expect(page). + to have_content 'moncefsamaritanhouse.com is not a valid email' + end +end diff --git a/spec/support/features/form_helpers.rb b/spec/support/features/form_helpers.rb index 431015933..b7c86d10c 100644 --- a/spec/support/features/form_helpers.rb +++ b/spec/support/features/form_helpers.rb @@ -48,7 +48,7 @@ def add_contact(options = {}) end def update_contact(options = {}) - within('#contacts') do + within('.contacts') do fill_in find(:xpath, './/input[contains(@name, "[name]")]')[:id], with: options[:name] fill_in find(:xpath, './/input[contains(@name, "[title]")]')[:id], with: options[:title] fill_in find(:xpath, './/input[contains(@name, "[email]")]')[:id], with: options[:email] @@ -62,5 +62,37 @@ def delete_contact click_link 'Delete this contact permanently' click_button 'Save changes' end + + def add_fax(options = {}) + click_link 'Add a fax number' + update_fax(options) + end + + def update_fax(options = {}) + within('.faxes') do + fill_in find(:xpath, './/input[contains(@name, "[number]")]')[:id], with: options[:number] + fill_in find(:xpath, './/input[contains(@name, "[department]")]')[:id], with: options[:department] + end + end + + def delete_fax + click_link 'Delete this fax permanently' + click_button 'Save changes' + end + + def add_two_admins + click_link 'Add an admin email' + fill_in 'location[admin_emails][]', with: 'moncef@foo.com' + click_link 'Add an admin email' + admins = all(:xpath, "//input[contains(@name, '[admin_emails]')]") + fill_in admins[-1][:id], with: 'moncef@otherlocation.com' + click_button 'Save changes' + end + + def delete_all_admins + find_link('Delete this admin permanently', match: :first).click + find_link('Delete this admin permanently', match: :first).click + click_button 'Save changes' + end end end From c678993508c77cf9c5dc6df5e8b9155c7a007af8 Mon Sep 17 00:00:00 2001 From: Moncef Belyamani Date: Tue, 8 Jul 2014 14:40:08 -0400 Subject: [PATCH 13/17] Add remaining specs for updating a location --- app/assets/javascripts/form.js.coffee | 4 +- app/models/fax.rb | 2 + app/models/location.rb | 3 +- .../locations/forms/_contact_fields.html.haml | 4 +- .../locations/forms/_fax_fields.html.haml | 4 +- .../locations/forms/_phone_fields.html.haml | 9 +- spec/api/create_fax_spec.rb | 9 +- .../ability_to_add_admin_email_spec.rb | 42 ++++++ ...ns_spec.rb => update_admin_emails_spec.rb} | 4 - .../locations/update_description_spec.rb | 21 +++ .../admin/locations/update_emails_spec.rb | 61 ++++++++ .../locations/update_fax_numbers_spec.rb | 4 +- .../admin/locations/update_hours_spec.rb | 21 +++ .../locations/update_mail_address_spec.rb | 92 ++++++++++++ .../admin/locations/update_name_spec.rb | 22 +++ .../locations/update_phone_numbers_spec.rb | 132 ++++++++++++++++++ .../update_short_description_spec.rb | 22 +++ .../locations/update_transportation_spec.rb | 22 +++ .../admin/locations/update_urls_spec.rb | 46 ++++++ spec/support/features/form_helpers.rb | 66 +++++++++ 20 files changed, 566 insertions(+), 24 deletions(-) create mode 100644 spec/features/admin/locations/ability_to_add_admin_email_spec.rb rename spec/features/admin/locations/{update_location_admins_spec.rb => update_admin_emails_spec.rb} (89%) create mode 100644 spec/features/admin/locations/update_description_spec.rb create mode 100644 spec/features/admin/locations/update_emails_spec.rb create mode 100644 spec/features/admin/locations/update_hours_spec.rb create mode 100644 spec/features/admin/locations/update_mail_address_spec.rb create mode 100644 spec/features/admin/locations/update_name_spec.rb create mode 100644 spec/features/admin/locations/update_phone_numbers_spec.rb create mode 100644 spec/features/admin/locations/update_short_description_spec.rb create mode 100644 spec/features/admin/locations/update_transportation_spec.rb create mode 100644 spec/features/admin/locations/update_urls_spec.rb diff --git a/app/assets/javascripts/form.js.coffee b/app/assets/javascripts/form.js.coffee index aac55b85a..45a54264d 100644 --- a/app/assets/javascripts/form.js.coffee +++ b/app/assets/javascripts/form.js.coffee @@ -5,7 +5,7 @@ jQuery -> event.preventDefault() $('.edit_location').on 'click', '.delete_attribute', (event) -> - $(this).closest('fieldset').find("input").val('') + $(this).closest('fieldset').find('input').val('') $(this).closest('fieldset').hide() event.preventDefault() @@ -18,7 +18,7 @@ jQuery -> $('.edit_location').on 'click', '.add_array_fields', (event) -> time = new Date().getTime() $(this).before($(this).data('fields')) - inputs = $(this).parent().find('input[type=text]') + inputs = $(this).parent().find('input') inputs[inputs.length - 1].setAttribute('id', time) event.preventDefault() diff --git a/app/models/fax.rb b/app/models/fax.rb index 1e83ac54c..982f59deb 100644 --- a/app/models/fax.rb +++ b/app/models/fax.rb @@ -1,4 +1,6 @@ class Fax < ActiveRecord::Base + default_scope { order('id ASC') } + attr_accessible :number, :department belongs_to :location, touch: true diff --git a/app/models/location.rb b/app/models/location.rb index 6a77b6696..dc7c46c03 100644 --- a/app/models/location.rb +++ b/app/models/location.rb @@ -25,7 +25,8 @@ class Location < ActiveRecord::Base accepts_nested_attributes_for :mail_address, allow_destroy: true has_many :phones, dependent: :destroy - accepts_nested_attributes_for :phones, allow_destroy: true + accepts_nested_attributes_for :phones, + allow_destroy: true, reject_if: :all_blank has_many :services, dependent: :destroy accepts_nested_attributes_for :services, allow_destroy: true diff --git a/app/views/admin/locations/forms/_contact_fields.html.haml b/app/views/admin/locations/forms/_contact_fields.html.haml index 820cd1ca9..83f462eac 100644 --- a/app/views/admin/locations/forms/_contact_fields.html.haml +++ b/app/views/admin/locations/forms/_contact_fields.html.haml @@ -9,13 +9,13 @@ = f.text_field :email, maxlength: 255, class: 'span5' %br = f.label :phone, 'Phone' - = f.text_field :phone, maxlength: 12, class: 'span2' + = f.phone_field :phone, maxlength: 12, class: 'span2' %br = f.label :extension, 'Extension' = f.text_field :extension, maxlength: 12, class: 'span2' %br = f.label :fax, 'Fax' - = f.text_field :fax, maxlength: 12, class: 'span2' + = f.phone_field :fax, maxlength: 12, class: 'span2' = f.hidden_field :_destroy %br diff --git a/app/views/admin/locations/forms/_fax_fields.html.haml b/app/views/admin/locations/forms/_fax_fields.html.haml index 2692915cc..c35ef488f 100644 --- a/app/views/admin/locations/forms/_fax_fields.html.haml +++ b/app/views/admin/locations/forms/_fax_fields.html.haml @@ -1,6 +1,6 @@ = field_set_tag do - = f.label :name, 'Number' - = f.text_field :number, maxlength: 12, class: 'span2', autocomplete: true + = f.label :number, 'Number' + = f.phone_field :number, maxlength: 12, class: 'span2', autocomplete: true %br = f.label :department, 'Department' = f.text_field :department, maxlength: 50, class: 'span4' diff --git a/app/views/admin/locations/forms/_phone_fields.html.haml b/app/views/admin/locations/forms/_phone_fields.html.haml index 9adcbe0ac..4fd39c48e 100644 --- a/app/views/admin/locations/forms/_phone_fields.html.haml +++ b/app/views/admin/locations/forms/_phone_fields.html.haml @@ -1,9 +1,12 @@ = field_set_tag do = f.label :number, 'Number' - = f.text_field :number, maxlength: 12, class: 'span2' + = f.telephone_field :number, maxlength: 12, class: 'span2' %br = f.label :vanity_number, 'Vanity Number (for example: 650-123-HELP)' - = f.text_field :vanity_number, maxlength: 12, class: 'span2' + = f.telephone_field :vanity_number, maxlength: 12, class: 'span2' + %br + = f.label :number_type, 'Number Type (to identify TTY numbers)' + = f.select :number_type, [["TTY", "TTY"]], include_blank: true %br = f.label :extension, 'Extension' = f.text_field :extension, maxlength: 8, class: 'span2' @@ -13,4 +16,4 @@ = f.hidden_field :_destroy %br - = link_to "Delete this phone permanently", '#', class: "btn btn-danger delete_association" \ No newline at end of file + = link_to 'Delete this phone permanently', '#', class: 'btn btn-danger delete_association' \ No newline at end of file diff --git a/spec/api/create_fax_spec.rb b/spec/api/create_fax_spec.rb index 438fcfb68..2f6975986 100644 --- a/spec/api/create_fax_spec.rb +++ b/spec/api/create_fax_spec.rb @@ -1,18 +1,11 @@ require 'rails_helper' describe 'POST /locations/:location_id/faxes' do - before(:all) do - @loc = create(:location) - end - before(:each) do + @loc = create(:location) @fax_attributes = { number: '123-456-7890' } end - after(:all) do - Organization.find_each(&:destroy) - end - it 'creates a fax with valid attributes' do post( api_location_faxes_url(@loc, subdomain: ENV['API_SUBDOMAIN']), diff --git a/spec/features/admin/locations/ability_to_add_admin_email_spec.rb b/spec/features/admin/locations/ability_to_add_admin_email_spec.rb new file mode 100644 index 000000000..37413f57b --- /dev/null +++ b/spec/features/admin/locations/ability_to_add_admin_email_spec.rb @@ -0,0 +1,42 @@ +require 'rails_helper' + +describe 'ability to add an admin to a location' do + before(:each) do + @location = create(:location) + end + + context 'when neither super admin nor location admin' do + it "doesn't allow adding an admin" do + login_admin + visit '/admin/locations/vrs-services' + expect(page).to_not have_content 'Add an admin email' + end + end + + context 'when super admin' do + it 'allows adding an admin' do + login_super_admin + visit '/admin/locations/vrs-services' + expect(page).to have_content 'Add an admin email' + end + end + + context 'when admin belongs to org' do + it 'allows adding an admin' do + create(:location_for_org_admin) + login_admin + visit '/admin/locations/samaritan-house' + expect(page).to have_content 'Add an admin email' + end + end + + context 'when location admin' do + it 'allows adding an admin' do + new_admin = create(:admin_with_generic_email) + @location.update!(admin_emails: ['moncef@gmail.com']) + login_as_admin(new_admin) + visit '/admin/locations/vrs-services' + expect(page).to have_content 'Add an admin email' + end + end +end diff --git a/spec/features/admin/locations/update_location_admins_spec.rb b/spec/features/admin/locations/update_admin_emails_spec.rb similarity index 89% rename from spec/features/admin/locations/update_location_admins_spec.rb rename to spec/features/admin/locations/update_admin_emails_spec.rb index fa9248c75..5ec379fdb 100644 --- a/spec/features/admin/locations/update_location_admins_spec.rb +++ b/spec/features/admin/locations/update_admin_emails_spec.rb @@ -14,19 +14,16 @@ scenario 'by adding 2 new admins', :js do add_two_admins - visit '/admin/locations/vrs-services' total_admins = page. all(:xpath, "//input[@name='location[admin_emails][]']") expect(total_admins.length).to eq 2 delete_all_admins - visit '/admin/locations/vrs-services' expect(page).to have_no_xpath("//input[@name='location[admin_emails][]']") end scenario 'with empty admin', :js do click_link 'Add an admin' click_button 'Save changes' - visit '/admin/locations/vrs-services' expect(page).to have_no_xpath("//input[@name='location[admin_emails][]']") end @@ -35,7 +32,6 @@ fill_in 'location[admin_emails][]', with: 'moncef@samaritanhouse.com' click_link 'Add an admin' click_button 'Save changes' - visit '/admin/locations/vrs-services' total_admins = all(:xpath, "//input[@name='location[admin_emails][]']") expect(total_admins.length).to eq 1 end diff --git a/spec/features/admin/locations/update_description_spec.rb b/spec/features/admin/locations/update_description_spec.rb new file mode 100644 index 000000000..6f2adf41b --- /dev/null +++ b/spec/features/admin/locations/update_description_spec.rb @@ -0,0 +1,21 @@ +require 'rails_helper' + +feature 'Update description' do + background do + create(:location) + login_super_admin + visit '/admin/locations/vrs-services' + end + + scenario 'with empty description' do + fill_in 'location_description', with: '' + click_button 'Save changes' + expect(page).to have_content "Description can't be blank for Location" + end + + scenario 'with valid description' do + fill_in 'location_description', with: 'This is a description' + click_button 'Save changes' + expect(find_field('location_description').value).to eq 'This is a description' + end +end diff --git a/spec/features/admin/locations/update_emails_spec.rb b/spec/features/admin/locations/update_emails_spec.rb new file mode 100644 index 000000000..038281555 --- /dev/null +++ b/spec/features/admin/locations/update_emails_spec.rb @@ -0,0 +1,61 @@ +require 'rails_helper' + +feature 'Update emails' do + background do + @location = create(:location) + login_super_admin + visit '/admin/locations/vrs-services' + end + + scenario 'with empty email', :js do + click_link 'Add a general email' + click_button 'Save changes' + expect(page).to have_no_xpath("//input[@name='location[emails][]']") + end + + scenario 'with valid email', :js do + click_link 'Add a general email' + fill_in 'location[emails][]', with: 'moncefbelyamani@samaritanhousesanmateo.org' + click_button 'Save changes' + expect(find_field('location[emails][]').value). + to eq 'moncefbelyamani@samaritanhousesanmateo.org' + end + + scenario 'when clearing out existing email but not deleting it' do + @location.update!(emails: ['foo@ruby.org']) + visit '/admin/locations/vrs-services' + fill_in 'location[emails][]', with: '' + click_button 'Save changes' + expect(page).to have_no_xpath("//input[@name='location[emails][]']") + end + + scenario 'with an invalid email' do + @location.update!(emails: ['foo@ruby.org']) + visit '/admin/locations/vrs-services' + fill_in 'location_emails_0', with: 'example.org' + click_button 'Save changes' + expect(page).to have_content 'example.org is not a valid email' + end + + scenario 'by adding 2 new emails', :js do + add_two_emails + + emails = all(:xpath, "//input[@name='location[emails][]']") + expect(emails.length).to eq 2 + + email_id = emails[-1][:id] + expect(find_field(email_id).value).to eq 'ruby@foo.com' + + delete_all_emails + expect(page).to have_no_xpath("//input[@name='location[emails][]']") + end + + scenario 'with 2 emails but one is empty', :js do + @location.update!(emails: ['foo@ruby.org']) + visit '/admin/locations/vrs-services' + click_link 'Add a general email' + click_button 'Save changes' + total_emails = all(:xpath, "//input[@name='location[emails][]']") + expect(total_emails.length).to eq 1 + end +end diff --git a/spec/features/admin/locations/update_fax_numbers_spec.rb b/spec/features/admin/locations/update_fax_numbers_spec.rb index d350e8d68..05a8d1216 100644 --- a/spec/features/admin/locations/update_fax_numbers_spec.rb +++ b/spec/features/admin/locations/update_fax_numbers_spec.rb @@ -7,7 +7,7 @@ visit '/admin/locations/vrs-services' end - scenario 'when no faxes exist' do + scenario 'when no faxes exist' do within('.faxes') do expect(page). to have_no_xpath('.//input[contains(@name, "[number]")]') @@ -58,7 +58,7 @@ number: '123-456-7890', department: 'Director of Development' ) - click_link 'Add a fax' + click_link 'Add a fax number' within('.faxes') do all_faxes = all(:xpath, './/input[contains(@name, "[department]")]') fill_in all_faxes[-1][:id], with: 'Department' diff --git a/spec/features/admin/locations/update_hours_spec.rb b/spec/features/admin/locations/update_hours_spec.rb new file mode 100644 index 000000000..8ce8d1e73 --- /dev/null +++ b/spec/features/admin/locations/update_hours_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +feature 'Update hours' do + background do + create(:location) + login_super_admin + visit '/admin/locations/vrs-services' + end + + scenario 'with empty hours' do + fill_in 'location_hours', with: '' + click_button 'Save changes' + expect(find_field('location_hours').value).to be_nil + end + + scenario 'with valid hours' do + fill_in 'location_hours', with: 'Monday-Friday 10am-5pm' + click_button 'Save changes' + expect(find_field('location_hours').value).to eq 'Monday-Friday 10am-5pm' + end +end diff --git a/spec/features/admin/locations/update_mail_address_spec.rb b/spec/features/admin/locations/update_mail_address_spec.rb new file mode 100644 index 000000000..1305cdbc6 --- /dev/null +++ b/spec/features/admin/locations/update_mail_address_spec.rb @@ -0,0 +1,92 @@ +require 'rails_helper' + +feature 'Updating mailing address' do + before(:each) do + @location = create(:location) + login_super_admin + visit '/admin/locations/vrs-services' + end + + scenario 'adding a new mailing address with valid values', :js do + add_mailing_address( + attention: 'moncef', + street: '123', + city: 'Vienna', + state: 'VA', + zip: '12345' + ) + visit '/admin/locations/vrs-services' + + expect(find_field('location_mail_address_attributes_attention').value). + to eq 'moncef' + expect(find_field('location_mail_address_attributes_street').value). + to eq '123' + expect(find_field('location_mail_address_attributes_city').value). + to eq 'Vienna' + expect(find_field('location_mail_address_attributes_state').value). + to eq 'VA' + expect(find_field('location_mail_address_attributes_zip').value). + to eq '12345' + + remove_mail_address + visit '/admin/locations/vrs-services' + expect(page).to have_link 'Add a mailing address' + end + + scenario 'when leaving location without address or mail address', :js do + remove_street_address + expect(page). + to have_content 'A location must have at least one address type' + end +end + +feature 'Updating mailing address with invalid values' do + before(:all) do + @location = create(:no_address) + end + + before(:each) do + login_super_admin + visit '/admin/locations/no-address' + end + + after(:all) do + Organization.find_each(&:destroy) + end + + scenario 'with an empty street' do + update_mailing_address(street: '', city: 'fair', state: 'VA', zip: '12345') + click_button 'Save changes' + expect(page).to have_content "street can't be blank for Mail Address" + end + + scenario 'with an empty city' do + update_mailing_address(street: '123', city: '', state: 'VA', zip: '12345') + click_button 'Save changes' + expect(page).to have_content "city can't be blank for Mail Address" + end + + scenario 'with an empty state' do + update_mailing_address(street: '123', city: 'fair', state: '', zip: '12345') + click_button 'Save changes' + expect(page).to have_content "state can't be blank for Mail Address" + end + + scenario 'with an empty zip' do + update_mailing_address(street: '123', city: 'Belmont', state: 'CA', zip: '') + click_button 'Save changes' + expect(page).to have_content "zip can't be blank for Mail Address" + end + + scenario 'with an invalid state' do + update_mailing_address(street: '123', city: 'Par', state: 'V', zip: '12345') + click_button 'Save changes' + expect(page).to have_content 'valid 2-letter state abbreviation' + end + + scenario 'with an invalid zip' do + update_mailing_address(street: '123', city: 'Ald', state: 'VA', zip: '1234') + click_button 'Save changes' + expect(page).to have_content 'valid ZIP code' + end +end diff --git a/spec/features/admin/locations/update_name_spec.rb b/spec/features/admin/locations/update_name_spec.rb new file mode 100644 index 000000000..35b209210 --- /dev/null +++ b/spec/features/admin/locations/update_name_spec.rb @@ -0,0 +1,22 @@ +require 'rails_helper' + +feature 'Update name' do + background do + create(:location) + login_super_admin + visit '/admin/locations/vrs-services' + end + + scenario 'with empty location name' do + fill_in 'location_name', with: '' + click_button 'Save changes' + expect(page).to have_content "Name can't be blank for Location" + end + + scenario 'with valid location name' do + fill_in 'location_name', with: 'Juvenile Sexual Responsibility Program' + click_button 'Save changes' + expect(find_field('location_name').value). + to eq 'Juvenile Sexual Responsibility Program' + end +end diff --git a/spec/features/admin/locations/update_phone_numbers_spec.rb b/spec/features/admin/locations/update_phone_numbers_spec.rb new file mode 100644 index 000000000..50f57255c --- /dev/null +++ b/spec/features/admin/locations/update_phone_numbers_spec.rb @@ -0,0 +1,132 @@ +require 'rails_helper' + +feature 'Update phones' do + background do + @location = create(:location) + login_super_admin + visit '/admin/locations/vrs-services' + end + + scenario 'when no phones exist' do + within('.phones') do + expect(page). + to have_no_xpath('.//input[contains(@name, "[extension]")]') + end + + expect(page).to_not have_link 'Delete this phone permanently' + end + + scenario 'by adding a new phone', :js do + add_phone( + number: '123-456-7890', + number_type: 'TTY', + department: 'Director of Development', + extension: 'x1234', + vanity_number: '123-ABC-DEFG' + ) + click_button 'Save changes' + visit '/admin/locations/vrs-services' + + expect(find_field('location_phones_attributes_0_number').value). + to eq '123-456-7890' + + expect(find_field('location_phones_attributes_0_number_type').value). + to eq 'TTY' + + expect(find_field('location_phones_attributes_0_department').value). + to eq 'Director of Development' + + expect(find_field('location_phones_attributes_0_extension').value). + to eq 'x1234' + + expect(find_field('location_phones_attributes_0_vanity_number').value). + to eq '123-ABC-DEFG' + + delete_phone + visit '/admin/locations/vrs-services' + within('.phones') do + expect(page). + to have_no_xpath('.//input[contains(@name, "[vanity_number]")]') + end + end + + scenario 'with 2 phones but one is empty', :js do + add_phone( + number: '123-456-7890', + department: 'Director of Development' + ) + click_link 'Add a phone number' + click_button 'Save changes' + + within('.phones') do + total_phones = all(:xpath, './/input[contains(@name, "[number]")]') + expect(total_phones.length).to eq 1 + end + end + + scenario 'with 2 phones but second one is invalid', :js do + add_phone( + number: '123-456-7890', + department: 'Director of Development' + ) + click_link 'Add a phone number' + within('.phones') do + all_phones = all(:xpath, './/input[contains(@name, "[department]")]') + fill_in all_phones[-1][:id], with: 'Department' + end + click_button 'Save changes' + expect(page).to have_content "number can't be blank for Phone" + end + + scenario 'delete second phone', :js do + # There was a bug where clicking the "Delete this phone permanently" + # button would hide all phones from the form, and would set the id + # of the first phone to "1", causing an error when submitting the form. + # To test this, we need to make sure neither of the phones we are trying + # to delete have an id of 1, so we need to create 3 phones first and + # test with the last 2. + @location.phones.create!(number: '124-456-7890', department: 'Ruby') + new_loc = create(:nearby_loc) + new_loc.phones.create!(number: '123-456-7890', department: 'Python') + new_loc.phones.create!(number: '456-123-7890', department: 'JS') + + visit '/admin/locations/library' + + find(:xpath, "(//a[text()='Delete this phone permanently'])[2]").click + click_button 'Save changes' + + expect(find_field('location_phones_attributes_0_number').value). + to eq '123-456-7890' + + expect(page). + to have_no_xpath('//input[@id="location_phones_attributes_1_number"]') + end +end + +feature 'Update phones' do + before(:all) do + @location = create(:location) + @location.phones.create!(number: '123-456-7890', department: 'Ruby') + end + + before(:each) do + login_super_admin + visit '/admin/locations/vrs-services' + end + + after(:all) do + Organization.find_each(&:destroy) + end + + scenario 'with an empty number' do + update_phone(number: '') + click_button 'Save changes' + expect(page).to have_content "number can't be blank for Phone" + end + + scenario 'with an invalid number' do + update_phone(number: '703') + click_button 'Save changes' + expect(page).to have_content 'is not a valid US phone number' + end +end diff --git a/spec/features/admin/locations/update_short_description_spec.rb b/spec/features/admin/locations/update_short_description_spec.rb new file mode 100644 index 000000000..265ddbdbd --- /dev/null +++ b/spec/features/admin/locations/update_short_description_spec.rb @@ -0,0 +1,22 @@ +require 'rails_helper' + +feature 'Update short description' do + background do + create(:location) + login_super_admin + visit '/admin/locations/vrs-services' + end + + scenario 'with empty description' do + fill_in 'location_short_desc', with: '' + click_button 'Save changes' + expect(find_field('location_short_desc').value).to eq '' + end + + scenario 'with valid description' do + fill_in 'location_short_desc', with: 'This is a short description' + click_button 'Save changes' + expect(find_field('location_short_desc').value). + to eq 'This is a short description' + end +end diff --git a/spec/features/admin/locations/update_transportation_spec.rb b/spec/features/admin/locations/update_transportation_spec.rb new file mode 100644 index 000000000..479a4eaef --- /dev/null +++ b/spec/features/admin/locations/update_transportation_spec.rb @@ -0,0 +1,22 @@ +require 'rails_helper' + +feature 'Update transportation options' do + background do + create(:location) + login_super_admin + visit '/admin/locations/vrs-services' + end + + scenario 'with empty transportation options' do + fill_in 'location_transportation', with: '' + click_button 'Save changes' + expect(find_field('location_transportation').value).to eq '' + end + + scenario 'with non-empty transportation options' do + fill_in 'location_transportation', with: 'SAMTRANS stops within 1/2 mile.' + click_button 'Save changes' + expect(find_field('location_transportation').value). + to eq 'SAMTRANS stops within 1/2 mile.' + end +end diff --git a/spec/features/admin/locations/update_urls_spec.rb b/spec/features/admin/locations/update_urls_spec.rb new file mode 100644 index 000000000..f42f88ebe --- /dev/null +++ b/spec/features/admin/locations/update_urls_spec.rb @@ -0,0 +1,46 @@ +require 'rails_helper' + +feature 'Update websites' do + background do + @location = create(:location) + login_super_admin + visit '/admin/locations/vrs-services' + end + + scenario 'when no websites exist' do + expect(page).to have_no_xpath("//input[@name='location[urls][]']") + end + + scenario 'by adding 2 new websites', :js do + add_two_urls + expect(find_field('location_urls_0').value).to eq 'http://ruby.com' + delete_all_urls + expect(page).to have_no_xpath("//input[@name='location[urls][]']") + end + + scenario 'with 2 urls but one is empty', :js do + @location.update!(urls: ['http://ruby.org']) + visit '/admin/locations/vrs-services' + click_link 'Add a website' + click_button 'Save changes' + total_urls = all(:xpath, "//input[@type='url']") + expect(total_urls.length).to eq 1 + end + + scenario 'with invalid website' do + @location.update!(urls: ['http://ruby.org']) + visit '/admin/locations/vrs-services' + fill_in 'location_urls_0', with: 'www.monfresh.com' + click_button 'Save changes' + expect(page).to have_content 'www.monfresh.com is not a valid URL' + end + + scenario 'with valid website' do + @location.update!(urls: ['http://ruby.org']) + visit '/admin/locations/vrs-services' + fill_in 'location_urls_0', with: 'http://codeforamerica.org' + click_button 'Save changes' + expect(find_field('location_urls_0').value). + to eq 'http://codeforamerica.org' + end +end diff --git a/spec/support/features/form_helpers.rb b/spec/support/features/form_helpers.rb index b7c86d10c..2cc0d8745 100644 --- a/spec/support/features/form_helpers.rb +++ b/spec/support/features/form_helpers.rb @@ -32,6 +32,21 @@ def update_street_address(options = {}) click_button 'Save changes' end + def add_mailing_address(options = {}) + click_link 'Add a mailing address' + update_mailing_address(options) + click_button 'Save changes' + end + + def update_mailing_address(options = {}) + fill_in 'location_mail_address_attributes_attention', with: options[:attention] + fill_in 'location_mail_address_attributes_street', with: options[:street] + fill_in 'location_mail_address_attributes_city', with: options[:city] + fill_in 'location_mail_address_attributes_state', with: options[:state] + fill_in 'location_mail_address_attributes_zip', with: options[:zip] + click_button 'Save changes' + end + def remove_street_address click_link 'Delete this address permanently' click_button 'Save changes' @@ -63,6 +78,21 @@ def delete_contact click_button 'Save changes' end + def add_two_emails + click_link 'Add a general email' + fill_in 'location[emails][]', with: 'foo@ruby.com' + click_link 'Add a general email' + emails = all(:xpath, "//input[@name='location[emails][]']") + fill_in emails[-1][:id], with: 'ruby@foo.com' + click_button 'Save changes' + end + + def delete_all_emails + find_link('Delete this email permanently', match: :first).click + find_link('Delete this email permanently', match: :first).click + click_button 'Save changes' + end + def add_fax(options = {}) click_link 'Add a fax number' update_fax(options) @@ -80,6 +110,27 @@ def delete_fax click_button 'Save changes' end + def add_phone(options = {}) + click_link 'Add a phone number' + update_phone(options) + end + + def update_phone(options = {}) + within('.phones') do + fill_in find(:xpath, './/input[contains(@name, "[number]")]')[:id], with: options[:number] + select_field = find(:xpath, './/select[contains(@name, "[number_type]")]')[:id] + select(options[:number_type], from: select_field) + fill_in find(:xpath, './/input[contains(@name, "[department]")]')[:id], with: options[:department] + fill_in find(:xpath, './/input[contains(@name, "[extension]")]')[:id], with: options[:extension] + fill_in find(:xpath, './/input[contains(@name, "[vanity_number]")]')[:id], with: options[:vanity_number] + end + end + + def delete_phone + click_link 'Delete this phone permanently' + click_button 'Save changes' + end + def add_two_admins click_link 'Add an admin email' fill_in 'location[admin_emails][]', with: 'moncef@foo.com' @@ -94,5 +145,20 @@ def delete_all_admins find_link('Delete this admin permanently', match: :first).click click_button 'Save changes' end + + def add_two_urls + click_link 'Add a website' + fill_in 'location[urls][]', with: 'http://ruby.com' + click_link 'Add a website' + urls = all(:xpath, "//input[@name='location[urls][]']") + fill_in urls[-1][:id], with: 'http://monfresh.com' + click_button 'Save changes' + end + + def delete_all_urls + find_link('Delete this website permanently', match: :first).click + find_link('Delete this website permanently', match: :first).click + click_button 'Save changes' + end end end From 50bcdf12f4c02824b26e79c28776eda27f8239ca Mon Sep 17 00:00:00 2001 From: Moncef Belyamani Date: Fri, 11 Jul 2014 17:01:42 -0700 Subject: [PATCH 14/17] Add CRUD functionality for Organizations. Other features: - Allow super admins to create locations and organizations - Allow regular admins to view and update locations and organizations separately - Optimize authorization code - Optimize loading of organization and locations - In the admin interface, admins can send an email to someone to get help. That email was hardcoded. Now it is customizable in settings.yml --- app/assets/javascripts/form.js.coffee | 10 +- app/assets/stylesheets/application.css.scss | 2 +- app/controllers/admin/locations_controller.rb | 35 ++-- .../admin/organizations_controller.rb | 77 +++++++ app/controllers/application_controller.rb | 2 +- app/decorators/admin/admin_decorator.rb | 42 ++-- app/helpers/admin/form_helper.rb | 4 +- app/models/admin.rb | 6 - app/models/concerns/search.rb | 24 ++- app/models/organization.rb | 10 +- app/views/admin/dashboard/index.html.haml | 12 ++ app/views/admin/locations/_form.html.haml | 3 +- app/views/admin/locations/edit.html.haml | 2 +- ...in_email_fields_for_new_location.html.haml | 4 + .../locations/forms/_admin_emails.html.haml | 30 +-- .../admin/locations/forms/_emails.html.haml | 2 +- .../forms/_new_location_form.html.haml | 22 +- .../admin/locations/forms/_urls.html.haml | 6 +- app/views/admin/locations/index.html.haml | 19 +- app/views/admin/locations/new.html.haml | 2 +- .../_confirm_delete_organization.html.haml | 16 ++ app/views/admin/organizations/_form.html.haml | 28 +++ .../confirm_delete_organization.js.erb | 1 + app/views/admin/organizations/edit.html.haml | 5 + .../admin/organizations/forms/_name.html.haml | 5 + .../forms/_new_organization_form.html.haml | 11 + .../organizations/forms/_url_fields.html.haml | 4 + .../admin/organizations/forms/_urls.html.haml | 16 ++ app/views/admin/organizations/index.html.haml | 16 ++ app/views/admin/organizations/new.html.haml | 5 + app/views/admins/shared/_navigation.html.haml | 2 + config/routes.rb | 3 + config/settings.example.yml | 9 +- config/settings.yml | 3 + spec/api/get_organizations_spec.rb | 4 +- spec/api/search_spec.rb | 78 ++++--- spec/features/admin/dashboard_spec.rb | 43 ++++ .../admin/locations/create_location_spec.rb | 194 ++++++++++++++++++ .../admin/locations/delete_location_spec.rb | 23 +++ .../admin/locations/visit_locations_spec.rb | 8 - .../organizations/create_organization_spec.rb | 32 +++ .../organizations/delete_organization_spec.rb | 23 +++ .../admin/organizations/update_name_spec.rb | 22 ++ .../admin/organizations/update_urls_spec.rb | 46 +++++ .../organizations/visit_organization_spec.rb | 74 +++++++ .../organizations/visit_organizations_spec.rb | 120 +++++++++++ spec/features/admin/sign_in_spec.rb | 15 +- spec/support/features/form_helpers.rb | 16 +- 48 files changed, 959 insertions(+), 177 deletions(-) create mode 100644 app/controllers/admin/organizations_controller.rb create mode 100644 app/views/admin/locations/forms/_admin_email_fields_for_new_location.html.haml create mode 100644 app/views/admin/organizations/_confirm_delete_organization.html.haml create mode 100644 app/views/admin/organizations/_form.html.haml create mode 100644 app/views/admin/organizations/confirm_delete_organization.js.erb create mode 100644 app/views/admin/organizations/edit.html.haml create mode 100644 app/views/admin/organizations/forms/_name.html.haml create mode 100644 app/views/admin/organizations/forms/_new_organization_form.html.haml create mode 100644 app/views/admin/organizations/forms/_url_fields.html.haml create mode 100644 app/views/admin/organizations/forms/_urls.html.haml create mode 100644 app/views/admin/organizations/index.html.haml create mode 100644 app/views/admin/organizations/new.html.haml create mode 100644 spec/features/admin/locations/create_location_spec.rb create mode 100644 spec/features/admin/locations/delete_location_spec.rb create mode 100644 spec/features/admin/organizations/create_organization_spec.rb create mode 100644 spec/features/admin/organizations/delete_organization_spec.rb create mode 100644 spec/features/admin/organizations/update_name_spec.rb create mode 100644 spec/features/admin/organizations/update_urls_spec.rb create mode 100644 spec/features/admin/organizations/visit_organization_spec.rb create mode 100644 spec/features/admin/organizations/visit_organizations_spec.rb diff --git a/app/assets/javascripts/form.js.coffee b/app/assets/javascripts/form.js.coffee index 45a54264d..eb65a616e 100644 --- a/app/assets/javascripts/form.js.coffee +++ b/app/assets/javascripts/form.js.coffee @@ -1,28 +1,28 @@ jQuery -> - $('.edit_location').on 'click', '.delete_association', (event) -> + $('.edit_entry').on 'click', '.delete_association', (event) -> $(this).prevAll('input[type=hidden]').val('1') $(this).closest('fieldset').hide() event.preventDefault() - $('.edit_location').on 'click', '.delete_attribute', (event) -> + $('.edit_entry').on 'click', '.delete_attribute', (event) -> $(this).closest('fieldset').find('input').val('') $(this).closest('fieldset').hide() event.preventDefault() - $('.edit_location').on 'click', '.add_fields', (event) -> + $('.edit_entry').on 'click', '.add_fields', (event) -> time = new Date().getTime() regexp = new RegExp($(this).data('id'), 'g') $(this).before($(this).data('fields').replace(regexp, time)) event.preventDefault() - $('.edit_location').on 'click', '.add_array_fields', (event) -> + $('.edit_entry').on 'click', '.add_array_fields', (event) -> time = new Date().getTime() $(this).before($(this).data('fields')) inputs = $(this).parent().find('input') inputs[inputs.length - 1].setAttribute('id', time) event.preventDefault() - $('.new_location').on 'click', '.add_fields', (event) -> + $('.new_entry').on 'click', '.add_fields', (event) -> time = new Date().getTime() regexp = new RegExp($(this).data('id'), 'g') $(this).before($(this).data('fields').replace(regexp, time)) diff --git a/app/assets/stylesheets/application.css.scss b/app/assets/stylesheets/application.css.scss index 5e35e4f43..38c8838ad 100644 --- a/app/assets/stylesheets/application.css.scss +++ b/app/assets/stylesheets/application.css.scss @@ -109,7 +109,7 @@ // margin added to accommodate for floating footer // This needs to be added to the last form element before the save button -.edit_location +.edit_entry, .new_entry { margin-bottom: 60px; } diff --git a/app/controllers/admin/locations_controller.rb b/app/controllers/admin/locations_controller.rb index 48a7f50a7..4d881bca4 100644 --- a/app/controllers/admin/locations_controller.rb +++ b/app/controllers/admin/locations_controller.rb @@ -5,24 +5,8 @@ class LocationsController < ApplicationController def index @admin_decorator = AdminDecorator.new(current_admin) - if current_admin.super_admin? - @locations = Location.page(params[:page]).per(params[:per_page]). - order('created_at DESC') - else - @locations = @admin_decorator.locations - @org = @locations.includes(:organization).first.organization if @locations.present? - end - end - - def new - @location = Location.new - @org = current_admin.org - if @org.present? - @location_url = @org.locations.map(&:urls).uniq.first - else - redirect_to admin_dashboard_path, - alert: "Sorry, you don't have access to that page." - end + @locations = Kaminari.paginate_array(@admin_decorator.locations). + page(params[:page]).per(params[:per_page]) end def edit @@ -53,18 +37,29 @@ def update end end + def new + @location = Location.new + @admin_decorator = AdminDecorator.new(current_admin) + @orgs = @admin_decorator.orgs + + unless @orgs.present? + redirect_to admin_dashboard_path, + alert: "Sorry, you don't have access to that page." + end + end + def create @location = Location.new(params[:location]) + @admin_decorator = AdminDecorator.new(current_admin) + @orgs = @admin_decorator.orgs respond_to do |format| if @location.save - @org = @location.organization format.html do redirect_to admin_locations_url, notice: 'Location was successfully created.' end else - @org = current_admin.org format.html { render :new } end end diff --git a/app/controllers/admin/organizations_controller.rb b/app/controllers/admin/organizations_controller.rb new file mode 100644 index 000000000..2a16d6bba --- /dev/null +++ b/app/controllers/admin/organizations_controller.rb @@ -0,0 +1,77 @@ +class Admin + class OrganizationsController < ApplicationController + before_action :authenticate_admin! + layout 'admin' + + def index + @admin_decorator = AdminDecorator.new(current_admin) + @orgs = Kaminari.paginate_array(@admin_decorator.orgs).page(params[:page]) + end + + def edit + @admin_decorator = AdminDecorator.new(current_admin) + @organization = Organization.find(params[:id]) + + unless @admin_decorator.allowed_to_access_organization?(@organization) + redirect_to admin_dashboard_path, + alert: "Sorry, you don't have access to that page." + end + end + + def update + @organization = Organization.find(params[:id]) + + respond_to do |format| + if @organization.update(params[:organization]) + format.html do + redirect_to [:admin, @organization], + notice: 'Organization was successfully updated.' + end + else + format.html { render :edit } + end + end + end + + def new + unless current_admin.super_admin? + redirect_to admin_dashboard_path, + alert: "Sorry, you don't have access to that page." + end + + @organization = Organization.new + end + + def create + @organization = Organization.new(params[:organization]) + + respond_to do |format| + if @organization.save + format.html do + redirect_to admin_organizations_url, + notice: 'Organization was successfully created.' + end + else + format.html { render :new } + end + end + end + + def destroy + organization = Organization.find(params[:id]) + organization.destroy + respond_to do |format| + format.html { redirect_to admin_organizations_path } + end + end + + def confirm_delete_organization + @org_name = params[:org_name] + @org_id = params[:org_id] + respond_to do |format| + format.html + format.js + end + end + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 32c912f7a..4facc51db 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -18,7 +18,7 @@ class ApplicationController < ActionController::Base def after_sign_in_path_for(resource) return root_url if resource.is_a?(User) - return admin_locations_path if resource.is_a?(Admin) + return admin_dashboard_path if resource.is_a?(Admin) end def after_sign_out_path_for(resource) diff --git a/app/decorators/admin/admin_decorator.rb b/app/decorators/admin/admin_decorator.rb index 6f7a485e1..a5cf96583 100644 --- a/app/decorators/admin/admin_decorator.rb +++ b/app/decorators/admin/admin_decorator.rb @@ -6,42 +6,30 @@ def initialize(admin) @admin = admin end - def allowed_to_access_location?(location) - return true if location_admin_emails_match_admin_email?(location) || admin.super_admin? - if admin_has_generic_email? - location_emails_match_admin_email?(location) + def locations + if admin.super_admin? + Location.pluck(:id, :name, :slug) else - location_emails_match_domain?(location) || location_urls_match_domain?(location) - end - end - - def domain - admin.email.split('@').last - end - - %w(urls emails).each do |name| - define_method "location_#{name}_match_domain?" do |location| - location.send(name).select { |attr| attr.include?(domain) }.present? + Location.text_search(email: admin.email).pluck(:id, :name, :slug) end end - %w(admin_emails emails).each do |name| - define_method "location_#{name}_match_admin_email?" do |location| - location.send(name).include?(admin.email) + def orgs + if admin.super_admin? + Organization.pluck(:id, :name, :slug) + else + Organization.joins(:locations). + where('locations.id IN (?)', locations.map(&:first).flatten). + uniq.pluck(:id, :name, :slug) end end - def admin_has_generic_email? - generic_domains = SETTINGS[:generic_domains] - generic_domains.include?(domain) + def allowed_to_access_location?(location) + locations.flatten.include?(location.id) end - def locations - if admin_has_generic_email? - Location.text_search(email: admin.email) - else - Location.text_search(domain: domain) - end + def allowed_to_access_organization?(org) + orgs.flatten.include?(org.id) end end end diff --git a/app/helpers/admin/form_helper.rb b/app/helpers/admin/form_helper.rb index 2f4c26cc8..a44b8001d 100644 --- a/app/helpers/admin/form_helper.rb +++ b/app/helpers/admin/form_helper.rb @@ -9,9 +9,9 @@ def link_to_add_fields(name, f, association) link_to(name, '#', class: 'add_fields btn btn-primary', data: { id: id, fields: fields.gsub('\n', '') }) end - def link_to_add_array_fields(name, field) + def link_to_add_array_fields(name, model, field) id = ''.object_id - fields = render("admin/locations/forms/#{field}_fields") + fields = render("admin/#{model}/forms/#{field}_fields") link_to(name, '#', class: 'add_array_fields btn btn-primary', data: { id: id, fields: fields.gsub('\n', '') }) end end diff --git a/app/models/admin.rb b/app/models/admin.rb index 5e3bc5a43..4e9f7e67f 100644 --- a/app/models/admin.rb +++ b/app/models/admin.rb @@ -9,10 +9,4 @@ class Admin < ActiveRecord::Base devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable, :confirmable - - def org - domain = email.split('@').last - admin_locs = Location.text_search(domain: domain) - admin_locs.includes(:organization).first.organization if admin_locs.present? - end end diff --git a/app/models/concerns/search.rb b/app/models/concerns/search.rb index 63679f526..2ed1585c9 100644 --- a/app/models/concerns/search.rb +++ b/app/models/concerns/search.rb @@ -23,15 +23,24 @@ module Search end end) - scope :has_email, ->(e) { where('admin_emails @@ :q or emails @@ :q', q: e) if e.present? } + scope :has_email, (lambda do |email| + if email.present? + return Location.none unless email.include?('@') - scope :has_domain, (lambda do |domain| - domain = domain.split('@').last if domain.present? + domain = email.split('@').last - if domain.present? && SETTINGS[:generic_domains].include?(domain) - Location.none - elsif domain.present? - where('urls ilike :q or emails ilike :q', q: "%#{domain}%") + locations = Location.arel_table + + if SETTINGS[:generic_domains].include?(domain) + # where('admin_emails @@ :q or emails @@ :q', q: email) + Location.where(locations[:admin_emails].matches("%#{email}%"). + or(locations[:emails].matches("%#{email}%"))) + else + # where('urls ilike :q or emails ilike :q or admin_emails @@ :p', q: "%#{domain}%", p: email) + Location.where(locations[:admin_emails].matches("%#{email}%"). + or(locations[:urls].matches("%#{domain}%")). + or(locations[:emails].matches("%#{domain}%"))) + end end end) @@ -57,7 +66,6 @@ def text_search(params = {}) has_category(params[:category]). belongs_to_org(params[:org_name]). has_email(params[:email]). - has_domain(params[:domain]). is_near(params[:location], params[:lat_lng], params[:radius]). has_keyword(params[:keyword]) end diff --git a/app/models/organization.rb b/app/models/organization.rb index 94663c033..5ce69ac69 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -1,5 +1,5 @@ class Organization < ActiveRecord::Base - default_scope { order('id ASC') } + default_scope { order('id DESC') } attr_accessible :name, :urls @@ -14,12 +14,18 @@ class Organization < ActiveRecord::Base # custom array validator. See app/validators/array_validator.rb validates :urls, array: { format: { with: %r{\Ahttps?://([^\s:@]+:[^\s:@]*@)?[A-Za-z\d\-]+(\.[A-Za-z\d\-]+)+\.?(:\d{1,5})?([\/?]\S*)?\z}i, - message: '%{value} is not a valid URL' } } + message: '%{value} is not a valid URL', allow_blank: true } } serialize :urls, Array auto_strip_attributes :name, squish: true + before_save :compact_urls + + def compact_urls + send('urls=', send('urls').reject(&:blank?)) if send('urls').is_a?(Array) + end + extend FriendlyId friendly_id :slug_candidates, use: [:history] diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 596e69afe..a8ab5120b 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -1,6 +1,18 @@ - if admin_signed_in? %p Welcome back, #{@admin.name}! + %p + Click the buttons below to manage organizations and locations. + %p + = link_to 'Organizations', admin_organizations_path, class: 'btn btn-primary' + %p + = link_to 'Locations', admin_locations_path, class: 'btn btn-primary' + + - if current_admin.super_admin? + %p + = link_to 'Add a new organization', new_admin_organization_path, class: 'btn btn-primary' + %p + = link_to 'Add a new location', new_admin_location_path, class: 'btn btn-primary' - else %p = t("titles.welcome", :brand => t("titles.brand")) diff --git a/app/views/admin/locations/_form.html.haml b/app/views/admin/locations/_form.html.haml index 4bb37ddb3..8c3005d71 100644 --- a/app/views/admin/locations/_form.html.haml +++ b/app/views/admin/locations/_form.html.haml @@ -6,7 +6,8 @@ %li= msg = render 'admin/locations/forms/location_name', f: f -= render 'admin/locations/forms/admin_emails', f: f +- if @admin_decorator.allowed_to_access_location?(@location) + = render 'admin/locations/forms/admin_emails', f: f = render 'admin/locations/forms/description', f: f = render 'admin/locations/forms/short_desc', f: f = render 'admin/locations/forms/address', f: f diff --git a/app/views/admin/locations/edit.html.haml b/app/views/admin/locations/edit.html.haml index f7932af49..f44991dd8 100644 --- a/app/views/admin/locations/edit.html.haml +++ b/app/views/admin/locations/edit.html.haml @@ -1,5 +1,5 @@ %div.content-box %h2= @location.name == @org.try(:name) ? @location.name : "#{@org.try(:name)} / #{@location.name}" -= form_for [:admin, @location] do |f| += form_for [:admin, @location], html: { class: 'edit_entry' } do |f| = render 'form', f: f diff --git a/app/views/admin/locations/forms/_admin_email_fields_for_new_location.html.haml b/app/views/admin/locations/forms/_admin_email_fields_for_new_location.html.haml new file mode 100644 index 000000000..6d410e078 --- /dev/null +++ b/app/views/admin/locations/forms/_admin_email_fields_for_new_location.html.haml @@ -0,0 +1,4 @@ += field_set_tag do + = text_field_tag 'location[admin_emails][]', current_admin.email, class: 'span4' + %br + = link_to "Delete this admin permanently", '#', class: "btn btn-danger delete_attribute" \ No newline at end of file diff --git a/app/views/admin/locations/forms/_admin_emails.html.haml b/app/views/admin/locations/forms/_admin_emails.html.haml index d16a99408..49c4b84e2 100644 --- a/app/views/admin/locations/forms/_admin_emails.html.haml +++ b/app/views/admin/locations/forms/_admin_emails.html.haml @@ -1,14 +1,16 @@ -- if @admin_decorator.allowed_to_access_location?(@location) - %div.inst-box.admin_emails - %header - %strong - Add an admin to this location - %p.desc - Which email addresses should be allowed to update and delete this location? - - if @location.admin_emails.present? - - @location.admin_emails.each_with_index do |admin, i| - = field_set_tag do - = text_field_tag 'location[admin_emails][]', admin, class: 'span4', id: "location_admin_emails_#{i}" - %br - = link_to "Delete this admin permanently", '#', class: "btn btn-danger delete_attribute" - = link_to_add_array_fields 'Add an admin email', :admin_email +%div.inst-box.admin_emails + %header + %strong + Add an admin to this location + %p.desc + Which email addresses should be allowed to update and delete this location? + + - if f.object.new_record? && !current_admin.super_admin? + = render 'admin/locations/forms/admin_email_fields_for_new_location' + - elsif @location.admin_emails.present? + - @location.admin_emails.each_with_index do |admin, i| + = field_set_tag do + = text_field_tag 'location[admin_emails][]', admin, class: 'span4', id: "location_admin_emails_#{i}" + %br + = link_to "Delete this admin permanently", '#', class: "btn btn-danger delete_attribute" + = link_to_add_array_fields 'Add an admin email', :locations, :admin_email diff --git a/app/views/admin/locations/forms/_emails.html.haml b/app/views/admin/locations/forms/_emails.html.haml index a55974765..91c13a179 100644 --- a/app/views/admin/locations/forms/_emails.html.haml +++ b/app/views/admin/locations/forms/_emails.html.haml @@ -11,4 +11,4 @@ = email_field_tag 'location[emails][]', email, class: 'span6', id: "location_emails_#{i}" %br = link_to "Delete this email permanently", '#', class: "btn btn-danger delete_attribute" - = link_to_add_array_fields 'Add a general email', :email + = link_to_add_array_fields 'Add a general email', :locations, :email diff --git a/app/views/admin/locations/forms/_new_location_form.html.haml b/app/views/admin/locations/forms/_new_location_form.html.haml index 590724bf8..43634cffd 100644 --- a/app/views/admin/locations/forms/_new_location_form.html.haml +++ b/app/views/admin/locations/forms/_new_location_form.html.haml @@ -5,6 +5,12 @@ - @location.errors.full_messages.each do |msg| %li= msg +%div.inst-box + %header + = f.label :organization_id, 'Choose an organization to create this location for.' + %p + = f.select :organization_id, @orgs.collect { |org| [org.second, org.first] }, include_blank: true + = render 'admin/locations/forms/location_name', f: f = render 'admin/locations/forms/admin_emails', f: f = render 'admin/locations/forms/description', f: f @@ -12,12 +18,12 @@ = render 'admin/locations/forms/address', f: f = render 'admin/locations/forms/mail_address', f: f = render 'admin/locations/forms/contacts', f: f -= render 'admin/locations/forms/urls', f: f, location_url: @location_url -= f.hidden_field :organization_id, value: @org.id += render 'admin/locations/forms/phones', f: f += render 'admin/locations/forms/faxes', f: f += render 'admin/locations/forms/emails', f: f += render 'admin/locations/forms/text_hours', f: f += render 'admin/locations/forms/transportation', f: f += render 'admin/locations/forms/urls', f: f += render 'admin/locations/forms/accessibility', f: f -%div.save-box.navbar-inner - %p - = 'Creating location for' - %strong - = @org.name - = f.submit 'Create location', class: 'btn btn-primary', data: { disable_with: 'Please wait...' } += f.submit 'Create location', class: 'btn btn-primary', data: { disable_with: 'Please wait...' } diff --git a/app/views/admin/locations/forms/_urls.html.haml b/app/views/admin/locations/forms/_urls.html.haml index 22375a4a3..ed60062cb 100644 --- a/app/views/admin/locations/forms/_urls.html.haml +++ b/app/views/admin/locations/forms/_urls.html.haml @@ -6,13 +6,11 @@ What websites are associated with the location? %em Must include "http://" or "https://" - - if f.object.new_record? - = url_field_tag "location[urls][]", @location_url, class: 'span9' - - elsif @location.urls.present? + - if @location.urls.present? - @location.urls.each_with_index do |url, i| = field_set_tag do = url_field_tag 'location[urls][]', url, class: 'span9', id: "location_urls_#{i}" %br = link_to "Delete this website permanently", '#', class: "btn btn-danger delete_attribute" - = link_to_add_array_fields 'Add a website', :url + = link_to_add_array_fields 'Add a website', :locations, :url diff --git a/app/views/admin/locations/index.html.haml b/app/views/admin/locations/index.html.haml index eabe46a5b..7baadd3f0 100644 --- a/app/views/admin/locations/index.html.haml +++ b/app/views/admin/locations/index.html.haml @@ -1,23 +1,18 @@ %p - Welcome back, #{current_admin.name}! -%p -Below you should see a list of locations that you are allowed to administer based on your email address. -If there are any locations missing, please #{mail_to "sanmateoco@codeforamerica.org", "let us know"}. -%p + Below you should see a list of locations that you are allowed to administer based on your email address. + If there are any locations missing, please #{mail_to "#{SETTINGS[:admin_support_email]}", "let us know"}. %p To start updating, click on one of the links, which will take you to the details page for the location. - if current_admin.super_admin? %p As a super admin, you have access to all locations in the database. Please make updates responsibly. -%br -- if !current_admin.super_admin? && @org.present? - %p - %strong="#{@org.name} locations:" + %p - @locations.each do |location| %ul - = link_to location.name, edit_admin_location_path(location) + = link_to location.second, edit_admin_location_path(location.third) + = paginate @locations -- if !current_admin.super_admin? && @org.present? +- if @locations.present? %p - = link_to "Add a new location", new_admin_location_path, :class => "btn btn-primary" + = link_to 'Add a new location', new_admin_location_path, class: 'btn btn-primary' diff --git a/app/views/admin/locations/new.html.haml b/app/views/admin/locations/new.html.haml index c537827f6..3813169c7 100644 --- a/app/views/admin/locations/new.html.haml +++ b/app/views/admin/locations/new.html.haml @@ -1,5 +1,5 @@ %div.content-box %h1 Create a new location -= form_for [:admin, @location], url: admin_locations_path, :html => {:method => :post} do |f| += form_for [:admin, @location], url: admin_locations_path, html: { method: :post, class: 'edit_entry' } do |f| = render 'admin/locations/forms/new_location_form', f: f diff --git a/app/views/admin/organizations/_confirm_delete_organization.html.haml b/app/views/admin/organizations/_confirm_delete_organization.html.haml new file mode 100644 index 000000000..2a66b01ac --- /dev/null +++ b/app/views/admin/organizations/_confirm_delete_organization.html.haml @@ -0,0 +1,16 @@ +%div.modal-header + %button.close{'aria-hidden' => 'true', 'data-dismiss' => 'modal', 'type' => 'button'} × + %h3#myModalLabel Are you ABSOLUTELY sure? +%div.modal-body + %p + = 'This action CANNOT be undone. This will delete ' + %strong + ="#{@org_name}" + = 'and all of its associated locations permanently.' + / %p + / Please type in the name of the location to confirm. + / %p + / = text_field_tag "location-name", "", class: "span5" +%div.modal-footer + %button.btn{'aria-hidden' => 'true', 'data-dismiss' => 'modal'} Close + = link_to 'I understand the consequences, delete this organization', { action: :destroy, id: @org_id }, method: :delete, class: 'btn btn-danger' diff --git a/app/views/admin/organizations/_form.html.haml b/app/views/admin/organizations/_form.html.haml new file mode 100644 index 000000000..2e03b3424 --- /dev/null +++ b/app/views/admin/organizations/_form.html.haml @@ -0,0 +1,28 @@ +- if @organization.errors.any? + .alert.alert-danger + %h2= "#{pluralize(@organization.errors.count, "error")} prohibited this organization from being saved:" + %ul + - @organization.errors.full_messages.each do |msg| + %li= msg + += render 'admin/organizations/forms/name', f: f += render 'admin/organizations/forms/urls', f: f + +%div.danger-zone + %header + %strong + Danger Zone + %h4 + Delete this organization + %p + Once you delete an organization, there is no going back. Please be certain. + %p + = link_to 'Permanently delete this organization', { action: :confirm_delete_organization, org_id: @organization.id, org_name: @organization.name }, remote: true, data: { toggle: 'modal', target: '#modal-window' }, class: 'boxed-action' +%div#modal-window.modal.hide.fade{'aria-hidden' => 'true', 'aria-labelledby' => 'myModalLabel', 'role' => 'dialog'} + +%div.save-box.navbar-inner + %p + = 'Editing' + %strong + = "#{@organization.name}" + = f.submit 'Save changes & apply edits to database', class: 'btn btn-success', data: { disable_with: 'Please wait...' } diff --git a/app/views/admin/organizations/confirm_delete_organization.js.erb b/app/views/admin/organizations/confirm_delete_organization.js.erb new file mode 100644 index 000000000..d8cc1a0cf --- /dev/null +++ b/app/views/admin/organizations/confirm_delete_organization.js.erb @@ -0,0 +1 @@ +$("#modal-window").html("<%= escape_javascript(render 'admin/organizations/confirm_delete_organization') %>"); \ No newline at end of file diff --git a/app/views/admin/organizations/edit.html.haml b/app/views/admin/organizations/edit.html.haml new file mode 100644 index 000000000..a34e1caf6 --- /dev/null +++ b/app/views/admin/organizations/edit.html.haml @@ -0,0 +1,5 @@ +%div.content-box + %h2= @organization.name += form_for [:admin, @organization], html: { class: 'edit_entry' } do |f| + = render 'admin/organizations/form', f: f + diff --git a/app/views/admin/organizations/forms/_name.html.haml b/app/views/admin/organizations/forms/_name.html.haml new file mode 100644 index 000000000..3f2034a33 --- /dev/null +++ b/app/views/admin/organizations/forms/_name.html.haml @@ -0,0 +1,5 @@ +%div.inst-box + %header + = f.label :name, 'Organization Name' + %p + = f.text_field :name, required: true, maxlength: 255, class: 'span10' \ No newline at end of file diff --git a/app/views/admin/organizations/forms/_new_organization_form.html.haml b/app/views/admin/organizations/forms/_new_organization_form.html.haml new file mode 100644 index 000000000..f9c27780f --- /dev/null +++ b/app/views/admin/organizations/forms/_new_organization_form.html.haml @@ -0,0 +1,11 @@ +- if @organization.errors.any? + .alert.alert-danger + %h2= "#{pluralize(@organization.errors.count, "error")} prohibited this organization from being saved:" + %ul + - @organization.errors.full_messages.each do |msg| + %li= msg + += render 'admin/organizations/forms/name', f: f += render 'admin/organizations/forms/urls', f: f + += f.submit 'Create organization', class: 'btn btn-primary', data: { disable_with: 'Please wait...' } diff --git a/app/views/admin/organizations/forms/_url_fields.html.haml b/app/views/admin/organizations/forms/_url_fields.html.haml new file mode 100644 index 000000000..c4d6bfbcf --- /dev/null +++ b/app/views/admin/organizations/forms/_url_fields.html.haml @@ -0,0 +1,4 @@ += field_set_tag do + = url_field_tag 'organization[urls][]', '', class: 'span9' + %br + = link_to "Delete this website permanently", '#', class: "btn btn-danger delete_attribute" diff --git a/app/views/admin/organizations/forms/_urls.html.haml b/app/views/admin/organizations/forms/_urls.html.haml new file mode 100644 index 000000000..9aaa1360e --- /dev/null +++ b/app/views/admin/organizations/forms/_urls.html.haml @@ -0,0 +1,16 @@ +%div.inst-box.urls + %header + %strong + Websites + %p.desc + What websites are associated with the organization? + %em + Must include "http://" or "https://" + - if @organization.urls.present? + - @organization.urls.each_with_index do |url, i| + = field_set_tag do + = url_field_tag 'organization[urls][]', url, class: 'span9', id: "organization_urls_#{i}" + %br + = link_to "Delete this website permanently", '#', class: "btn btn-danger delete_attribute" + = link_to_add_array_fields 'Add a website', :organizations, :url + diff --git a/app/views/admin/organizations/index.html.haml b/app/views/admin/organizations/index.html.haml new file mode 100644 index 000000000..acba1adb2 --- /dev/null +++ b/app/views/admin/organizations/index.html.haml @@ -0,0 +1,16 @@ +%p + Below you should see a list of organizations that you are allowed to administer. + If there are any entries missing, please #{mail_to "#{SETTINGS[:admin_support_email]}", "let us know"}. +%p + To start updating, click on one of the links, which will take you to the details page + for the organization. + +- if current_admin.super_admin? + %p + As a super admin, you have access to all locations in the database. Please make updates responsibly. + +%p + - @orgs.each do |org| + %ul + = link_to org.second, edit_admin_organization_path(org.third) + = paginate @orgs \ No newline at end of file diff --git a/app/views/admin/organizations/new.html.haml b/app/views/admin/organizations/new.html.haml new file mode 100644 index 000000000..9f5973942 --- /dev/null +++ b/app/views/admin/organizations/new.html.haml @@ -0,0 +1,5 @@ +%div.content-box + %h1 Create a new organization + += form_for [:admin, @organization], url: admin_organizations_path, html: { method: :post, class: 'edit_entry' } do |f| + = render 'admin/organizations/forms/new_organization_form', f: f diff --git a/app/views/admins/shared/_navigation.html.haml b/app/views/admins/shared/_navigation.html.haml index a09c12f0e..cc889801b 100644 --- a/app/views/admins/shared/_navigation.html.haml +++ b/app/views/admins/shared/_navigation.html.haml @@ -6,6 +6,8 @@ Logged in as #{current_admin.name} %li = link_to 'Edit account', edit_admin_registration_path + %li + = link_to 'Your organizations', admin_organizations_path %li = link_to 'Your locations', admin_locations_path %li diff --git a/config/routes.rb b/config/routes.rb index eab132002..8017c8a37 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -13,8 +13,11 @@ namespace :admin do root to: 'dashboard#index', as: :dashboard resources :locations, except: :show + resources :organizations, except: :show + get 'organizations/confirm_delete_organization', to: 'organizations#confirm_delete_organization', as: :confirm_delete_organization get 'locations/confirm_delete_location', to: 'locations#confirm_delete_location', as: :confirm_delete_location get 'locations/:id', to: 'locations#edit' + get 'organizations/:id', to: 'organizations#edit' end devise_for :admins, path: 'admin', controllers: { registrations: 'admin/registrations' } diff --git a/config/settings.example.yml b/config/settings.example.yml index fe0b8a599..3d6aef0f4 100644 --- a/config/settings.example.yml +++ b/config/settings.example.yml @@ -29,11 +29,11 @@ # This setting is used in 'app/models/concerns/search.rb'. bounds: [[25.7084, -124.085], [48.9084, -67.085]] -#################################################### +############################### # -# GENERIC EMAIL DOMAIN SETTINGS FOR ADMIN INTERFACE +# SETTINGS FOR ADMIN INTERFACE # -#################################################### +############################### # # An array of email domain names used by the admin interface to determine which # users get to access which locations. For convenience, the API allows a client @@ -61,6 +61,9 @@ generic_domains: - hotmail.com - yahoo.com +# The email that admin interface users should send questions/issues to. +admin_support_email: + ######################### # # SERVICE AREAS SETTINGS diff --git a/config/settings.yml b/config/settings.yml index fe0b8a599..69c62d96d 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -61,6 +61,9 @@ generic_domains: - hotmail.com - yahoo.com +# The email that admin interface users should send questions/issues to. +admin_support_email: ohanapi@codeforamerica.org + ######################### # # SERVICE AREAS SETTINGS diff --git a/spec/api/get_organizations_spec.rb b/spec/api/get_organizations_spec.rb index 4a099a6d7..b343905e8 100644 --- a/spec/api/get_organizations_spec.rb +++ b/spec/api/get_organizations_spec.rb @@ -24,9 +24,9 @@ expect(json.length).to eq(2) end - it 'sorts results by id ascending' do + it 'sorts results by id descending' do get api_organizations_url(subdomain: ENV['API_SUBDOMAIN']) - expect(json[1]['name']).to eq('Food Pantry') + expect(json.first['name']).to eq('Food Pantry') end it 'responds to pagination parameters' do diff --git a/spec/api/search_spec.rb b/spec/api/search_spec.rb index 3612bb0aa..6243b0992 100644 --- a/spec/api/search_spec.rb +++ b/spec/api/search_spec.rb @@ -344,115 +344,125 @@ end end - context 'with domain parameter' do + context 'when email parameter contains custom domain' do it "finds domain name when url contains 'www'" do create(:location, urls: ['http://www.smchsa.org']) create(:location, emails: ['info@cfa.org']) - get "#{api_search_index_url(subdomain: ENV['API_SUBDOMAIN'])}?domain=smchsa.org" + get "#{api_search_index_url(subdomain: ENV['API_SUBDOMAIN'])}?email=foo@smchsa.org" expect(headers['X-Total-Count']).to eq '1' end it 'finds naked domain name' do create(:location, urls: ['http://smchsa.com']) create(:location, emails: ['hello@cfa.com']) - get "#{api_search_index_url(subdomain: ENV['API_SUBDOMAIN'])}?domain=smchsa.com" + get "#{api_search_index_url(subdomain: ENV['API_SUBDOMAIN'])}?email=foo@smchsa.com" expect(headers['X-Total-Count']).to eq '1' end it 'finds long domain name in both url and email' do create(:location, urls: ['http://smchsa.org']) create(:location, emails: ['info@smchsa.org']) - get "#{api_search_index_url(subdomain: ENV['API_SUBDOMAIN'])}?domain=smchsa.org" + get "#{api_search_index_url(subdomain: ENV['API_SUBDOMAIN'])}?email=foo@smchsa.org" expect(headers['X-Total-Count']).to eq '2' end it 'finds domain name when URL contains path' do create(:location, urls: ['http://www.smchealth.org/mcah']) create(:location, emails: ['org@mcah.org']) - get "#{api_search_index_url(subdomain: ENV['API_SUBDOMAIN'])}?domain=smchealth.org" + get "#{api_search_index_url(subdomain: ENV['API_SUBDOMAIN'])}?email=foo@smchealth.org" expect(headers['X-Total-Count']).to eq '1' end it 'finds domain name when URL contains multiple paths' do create(:location, urls: ['http://www.smchsa.org/portal/site/planning']) create(:location, emails: ['sanmateo@ca.us']) - get "#{api_search_index_url(subdomain: ENV['API_SUBDOMAIN'])}?domain=smchsa.org" + get "#{api_search_index_url(subdomain: ENV['API_SUBDOMAIN'])}?email=foo@smchsa.org" expect(headers['X-Total-Count']).to eq '1' end it 'finds domain name when URL contains a dash' do create(:location, urls: ['http://www.childsup-connect.ca.gov']) create(:location, emails: ['gov@childsup-connect.gov']) - get "#{api_search_index_url(subdomain: ENV['API_SUBDOMAIN'])}?domain=childsup-connect.ca.gov" + get "#{api_search_index_url(subdomain: ENV['API_SUBDOMAIN'])}?email=foo@childsup-connect.ca.gov" expect(headers['X-Total-Count']).to eq '1' end it 'finds domain name when URL contains a number' do create(:location, urls: ['http://www.prenatalto3.org']) create(:location, emails: ['info@rwc2020.org']) - get "#{api_search_index_url(subdomain: ENV['API_SUBDOMAIN'])}?domain=prenatalto3.org" + get "#{api_search_index_url(subdomain: ENV['API_SUBDOMAIN'])}?email=foo@prenatalto3.org" expect(headers['X-Total-Count']).to eq '1' end + it 'returns locations where either emails or admins fields match' do + create(:location, emails: ['moncef@smcgov.org']) + create(:location_with_admin) + get api_search_index_url(email: 'moncef@smcgov.org', subdomain: ENV['API_SUBDOMAIN']) + expect(headers['X-Total-Count']).to eq '2' + end + + it 'does not return locations if email prefix is the only match' do + create(:location, emails: ['moncef@smcgov.org']) + create(:location_with_admin) + get api_search_index_url(email: 'moncef@gmail.com', subdomain: ENV['API_SUBDOMAIN']) + expect(headers['X-Total-Count']).to eq '0' + end + end + + context 'when email parameter contains generic domain' do it "doesn't return results for gmail domain" do create(:location, emails: ['info@gmail.com']) - get "#{api_search_index_url(subdomain: ENV['API_SUBDOMAIN'])}?domain=gmail.com" + get "#{api_search_index_url(subdomain: ENV['API_SUBDOMAIN'])}?email=foo@gmail.com" expect(headers['X-Total-Count']).to eq '0' end it "doesn't return results for aol domain" do create(:location, emails: ['info@aol.com']) - get "#{api_search_index_url(subdomain: ENV['API_SUBDOMAIN'])}?domain=aol.com" + get "#{api_search_index_url(subdomain: ENV['API_SUBDOMAIN'])}?email=foo@aol.com" expect(headers['X-Total-Count']).to eq '0' end it "doesn't return results for hotmail domain" do create(:location, emails: ['info@hotmail.com']) - get "#{api_search_index_url(subdomain: ENV['API_SUBDOMAIN'])}?domain=hotmail.com" + get "#{api_search_index_url(subdomain: ENV['API_SUBDOMAIN'])}?email=foo@hotmail.com" expect(headers['X-Total-Count']).to eq '0' end it "doesn't return results for yahoo domain" do create(:location, emails: ['info@yahoo.com']) - get "#{api_search_index_url(subdomain: ENV['API_SUBDOMAIN'])}?domain=yahoo.com" + get "#{api_search_index_url(subdomain: ENV['API_SUBDOMAIN'])}?email=foo@yahoo.com" expect(headers['X-Total-Count']).to eq '0' end it "doesn't return results for sbcglobal domain" do create(:location, emails: ['info@sbcglobal.net']) - get "#{api_search_index_url(subdomain: ENV['API_SUBDOMAIN'])}?domain=sbcglobal.net" + get "#{api_search_index_url(subdomain: ENV['API_SUBDOMAIN'])}?email=foo@sbcglobal.net" expect(headers['X-Total-Count']).to eq '0' end - it 'extracts domain name from parameter' do - create(:location, emails: ['info@sbcglobal.net']) - get "#{api_search_index_url(subdomain: ENV['API_SUBDOMAIN'])}?domain=info@sbcglobal.net" + it 'does not return locations if domain is the only match' do + create(:location, emails: ['moncef@gmail.com'], admin_emails: ['moncef@gmail.com']) + get api_search_index_url(email: 'foo@gmail.com', subdomain: ENV['API_SUBDOMAIN']) expect(headers['X-Total-Count']).to eq '0' end - end - context 'when email parameter only contains domain name' do - it "doesn't return results" do - create(:location, emails: ['info@gmail.com']) - get api_search_index_url(email: 'gmail.com', subdomain: ENV['API_SUBDOMAIN']) - expect(headers['X-Total-Count']).to eq '0' + it 'returns results if admin email matches parameter' do + create(:location, admin_emails: ['info@sbcglobal.net']) + get "#{api_search_index_url(subdomain: ENV['API_SUBDOMAIN'])}?email=info@sbcglobal.net" + expect(headers['X-Total-Count']).to eq '1' end - end - context 'when email parameter contains full email address' do - it 'returns locations where either emails or admins fields match' do - create(:location, emails: ['moncef@smcgov.org']) - create(:location_with_admin) - get api_search_index_url(email: 'moncef@smcgov.org', subdomain: ENV['API_SUBDOMAIN']) - expect(headers['X-Total-Count']).to eq '2' + it 'returns results if emails match parameter' do + create(:location, emails: ['info@sbcglobal.net']) + get "#{api_search_index_url(subdomain: ENV['API_SUBDOMAIN'])}?email=info@sbcglobal.net" + expect(headers['X-Total-Count']).to eq '1' end end - context 'when email parameter contains full email address' do - it 'only returns locations where admin email is exact match' do - create(:location, emails: ['moncef@smcgov.org']) - create(:location_with_admin) - get api_search_index_url(email: 'moncef@gmail.com', subdomain: ENV['API_SUBDOMAIN']) + context 'when email parameter only contains generic domain name' do + it "doesn't return results" do + create(:location, emails: ['info@gmail.com']) + get api_search_index_url(email: 'gmail.com', subdomain: ENV['API_SUBDOMAIN']) expect(headers['X-Total-Count']).to eq '0' end end diff --git a/spec/features/admin/dashboard_spec.rb b/spec/features/admin/dashboard_spec.rb index 17cd5c2bf..251a21b21 100644 --- a/spec/features/admin/dashboard_spec.rb +++ b/spec/features/admin/dashboard_spec.rb @@ -61,6 +61,20 @@ expect(page).to have_content 'Welcome back, Org Admin!' end + it 'includes a link to organizations in the body' do + within '.content' do + expect(page). + to have_link 'Organizations', href: admin_organizations_path + end + end + + it 'includes a link to locations in the body' do + within '.content' do + expect(page). + to have_link 'Locations', href: admin_locations_path + end + end + it 'does not include a link to the sign up page in the navigation' do within '.navbar' do expect(page).not_to have_link 'Sign up' @@ -98,5 +112,34 @@ expect(page).to have_link 'Your locations', href: admin_locations_path end end + + it 'includes a link to Your organizations in the navigation' do + within '.navbar' do + expect(page).to have_link 'Your organizations', href: admin_organizations_path + end + end + + it 'does not display a link to add a new organization' do + expect(page).not_to have_link 'Add a new organization', new_admin_organization_path + end + + it 'does not display a link to add a new location' do + expect(page).not_to have_link 'Add a new location', new_admin_location_path + end + end + + context 'when signed in as super admin' do + before :each do + login_super_admin + visit '/admin' + end + + it 'displays a link to add a new organization' do + expect(page).to have_link 'Add a new organization', new_admin_organization_path + end + + it 'displays a link to add a new location' do + expect(page).to have_link 'Add a new location', new_admin_location_path + end end end diff --git a/spec/features/admin/locations/create_location_spec.rb b/spec/features/admin/locations/create_location_spec.rb new file mode 100644 index 000000000..e6ccbdace --- /dev/null +++ b/spec/features/admin/locations/create_location_spec.rb @@ -0,0 +1,194 @@ +require 'rails_helper' + +feature 'Create a new location' do + background do + create(:organization) + login_super_admin + visit('/admin/locations/new') + end + + scenario 'with all required fields', :js do + fill_in_all_required_fields + click_button 'Create location' + click_link 'New Parent Agency location' + + expect(find_field('location_name').value).to eq 'New Parent Agency location' + expect(find_field('location_description').value).to eq 'new description' + expect(find_field('location_address_attributes_street').value). + to eq '123 Main St.' + expect(find_field('location_address_attributes_city').value). + to eq 'Belmont' + expect(find_field('location_address_attributes_state').value).to eq 'CA' + expect(find_field('location_address_attributes_zip').value).to eq '12345' + end + + scenario 'without any required fields' do + click_button 'Create location' + expect(page).to have_content "Description can't be blank for Location" + expect(page).to have_content "Name can't be blank for Location" + expect(page).to have_content 'A location must have at least one address type' + end + + scenario 'with valid mailing address', :js do + fill_in_all_required_fields + click_link 'Add a mailing address' + update_mailing_address( + attention: 'moncef', + street: '123', + city: 'Vienna', + state: 'VA', + zip: '12345' + ) + click_button 'Create location' + click_link 'New Parent Agency location' + + expect(find_field('location_mail_address_attributes_attention').value). + to eq 'moncef' + expect(find_field('location_mail_address_attributes_street').value). + to eq '123' + expect(find_field('location_mail_address_attributes_city').value). + to eq 'Vienna' + expect(find_field('location_mail_address_attributes_state').value). + to eq 'VA' + expect(find_field('location_mail_address_attributes_zip').value). + to eq '12345' + end + + scenario 'with valid phone number', :js do + fill_in_all_required_fields + add_phone( + number: '123-456-7890', + number_type: 'TTY', + department: 'Director of Development', + extension: 'x1234', + vanity_number: '123-ABC-DEFG' + ) + click_button 'Create location' + click_link 'New Parent Agency location' + + expect(find_field('location_phones_attributes_0_number').value). + to eq '123-456-7890' + + expect(find_field('location_phones_attributes_0_number_type').value). + to eq 'TTY' + + expect(find_field('location_phones_attributes_0_department').value). + to eq 'Director of Development' + + expect(find_field('location_phones_attributes_0_extension').value). + to eq 'x1234' + + expect(find_field('location_phones_attributes_0_vanity_number').value). + to eq '123-ABC-DEFG' + end + + scenario 'with valid fax number', :js do + fill_in_all_required_fields + add_fax( + number: '123-456-7890', + department: 'Director of Development' + ) + click_button 'Create location' + click_link 'New Parent Agency location' + + expect(find_field('location_faxes_attributes_0_number').value). + to eq '123-456-7890' + + expect(find_field('location_faxes_attributes_0_department').value). + to eq 'Director of Development' + end + + scenario 'with a valid contact', :js do + fill_in_all_required_fields + add_contact( + name: 'Moncef Belyamani-Belyamani', + title: 'Director of Development and Operations', + email: 'moncefbelyamani@samaritanhousesanmateo.org', + phone: '703-555-1212', + extension: 'x1234', + fax: '703-555-1234' + ) + click_button 'Create location' + click_link 'New Parent Agency location' + + expect(find_field('location_contacts_attributes_0_name').value). + to eq 'Moncef Belyamani-Belyamani' + + expect(find_field('location_contacts_attributes_0_title').value). + to eq 'Director of Development and Operations' + + expect(find_field('location_contacts_attributes_0_email').value). + to eq 'moncefbelyamani@samaritanhousesanmateo.org' + + expect(find_field('location_contacts_attributes_0_phone').value). + to eq '703-555-1212' + + expect(find_field('location_contacts_attributes_0_extension').value). + to eq 'x1234' + + expect(find_field('location_contacts_attributes_0_fax').value). + to eq '703-555-1234' + end + + scenario 'with valid location email', :js do + fill_in_all_required_fields + click_link 'Add a general email' + fill_in 'location[emails][]', with: 'moncefbelyamani@samaritanhousesanmateo.org' + + click_button 'Create location' + click_link 'New Parent Agency location' + + expect(find_field('location[emails][]').value). + to eq 'moncefbelyamani@samaritanhousesanmateo.org' + end + + scenario 'with valid location hours', :js do + fill_in_all_required_fields + fill_in 'location_hours', with: 'Monday-Friday 10am-5pm' + click_button 'Create location' + click_link 'New Parent Agency location' + + expect(find_field('location_hours').value).to eq 'Monday-Friday 10am-5pm' + end + + scenario 'when adding an accessibility option', :js do + fill_in_all_required_fields + check 'location_accessibility_elevator' + click_button 'Create location' + click_link 'New Parent Agency location' + + expect(find('#location_accessibility_elevator')).to be_checked + end + + scenario 'when adding transportation option', :js do + fill_in_all_required_fields + fill_in 'location_transportation', with: 'SAMTRANS stops within 1/2 mile.' + click_button 'Create location' + click_link 'New Parent Agency location' + + expect(find_field('location_transportation').value). + to eq 'SAMTRANS stops within 1/2 mile.' + end + + scenario 'when adding a website', :js do + fill_in_all_required_fields + click_link 'Add a website' + fill_in find(:xpath, "//input[@type='url']")[:id], with: 'http://ruby.com' + click_button 'Create location' + click_link 'New Parent Agency location' + + expect(find_field('location[urls][]').value).to eq 'http://ruby.com' + end +end + +describe 'creating a new location as regular admin' do + it 'prepopulates the current user as an admin for the new location' do + create(:location_for_org_admin) + login_admin + + visit('/admin/locations/new') + + expect(find_field('location[admin_emails][]').value). + to eq 'moncef@samaritanhouse.com' + end +end diff --git a/spec/features/admin/locations/delete_location_spec.rb b/spec/features/admin/locations/delete_location_spec.rb new file mode 100644 index 000000000..bdf6a0cf4 --- /dev/null +++ b/spec/features/admin/locations/delete_location_spec.rb @@ -0,0 +1,23 @@ +require 'rails_helper' + +feature 'Delete location' do + background do + create(:location) + login_super_admin + visit '/admin/locations/vrs-services' + end + + scenario 'when submitting warning', :js do + find_link('Permanently delete this location').click + find_link('I understand the consequences, delete this location').click + expect(current_path).to eq admin_locations_path + expect(page).not_to have_link 'VRS Services' + end + + scenario 'when canceling warning', :js do + find_link('Permanently delete this location').click + find_button('Close').click + visit admin_locations_path + expect(page).to have_link 'VRS Services' + end +end diff --git a/spec/features/admin/locations/visit_locations_spec.rb b/spec/features/admin/locations/visit_locations_spec.rb index f612fe3a6..a7af68f64 100644 --- a/spec/features/admin/locations/visit_locations_spec.rb +++ b/spec/features/admin/locations/visit_locations_spec.rb @@ -59,10 +59,6 @@ expect(page).to have_link 'Samaritan House' end - it 'greets the admin by their name' do - expect(page).to have_content 'Welcome back, Org Admin!' - end - it 'does not include a link to the sign up page in the navigation' do within '.navbar' do expect(page).not_to have_link 'Sign up' @@ -120,9 +116,5 @@ expect(page).to have_link 'Samaritan House' expect(page).not_to have_content 'Parent Agency locations' end - - it 'greets the admin by their name' do - expect(page).to have_content 'Welcome back, Super Admin!' - end end end diff --git a/spec/features/admin/organizations/create_organization_spec.rb b/spec/features/admin/organizations/create_organization_spec.rb new file mode 100644 index 000000000..381f21249 --- /dev/null +++ b/spec/features/admin/organizations/create_organization_spec.rb @@ -0,0 +1,32 @@ +require 'rails_helper' + +feature 'Create a new organization' do + background do + create(:organization) + login_super_admin + visit('/admin/organizations/new') + end + + scenario 'with all required fields' do + fill_in 'organization_name', with: 'new org' + click_button 'Create organization' + click_link 'new org' + + expect(find_field('organization_name').value).to eq 'new org' + end + + scenario 'without any required fields' do + click_button 'Create organization' + expect(page).to have_content "Name can't be blank for Organization" + end + + scenario 'when adding a website', :js do + fill_in 'organization_name', with: 'new org' + click_link 'Add a website' + fill_in find(:xpath, "//input[@type='url']")[:id], with: 'http://ruby.com' + click_button 'Create organization' + click_link 'new org' + + expect(find_field('organization[urls][]').value).to eq 'http://ruby.com' + end +end diff --git a/spec/features/admin/organizations/delete_organization_spec.rb b/spec/features/admin/organizations/delete_organization_spec.rb new file mode 100644 index 000000000..ee610247b --- /dev/null +++ b/spec/features/admin/organizations/delete_organization_spec.rb @@ -0,0 +1,23 @@ +require 'rails_helper' + +feature 'Delete organization' do + background do + create(:organization) + login_super_admin + visit '/admin/organizations/parent-agency' + end + + scenario 'when submitting warning', :js do + find_link('Permanently delete this organization').click + find_link('I understand the consequences, delete this organization').click + expect(current_path).to eq admin_organizations_path + expect(page).not_to have_link 'Parent Agency' + end + + scenario 'when canceling warning', :js do + find_link('Permanently delete this organization').click + find_button('Close').click + visit admin_organizations_path + expect(page).to have_link 'Parent Agency' + end +end diff --git a/spec/features/admin/organizations/update_name_spec.rb b/spec/features/admin/organizations/update_name_spec.rb new file mode 100644 index 000000000..79926e62e --- /dev/null +++ b/spec/features/admin/organizations/update_name_spec.rb @@ -0,0 +1,22 @@ +require 'rails_helper' + +feature 'Update name' do + background do + create(:organization) + login_super_admin + visit '/admin/organizations/parent-agency' + end + + scenario 'with empty organization name' do + fill_in 'organization_name', with: '' + click_button 'Save changes' + expect(page).to have_content "Name can't be blank for Organization" + end + + scenario 'with valid organization name' do + fill_in 'organization_name', with: 'Juvenile Sexual Responsibility Program' + click_button 'Save changes' + expect(find_field('organization_name').value). + to eq 'Juvenile Sexual Responsibility Program' + end +end diff --git a/spec/features/admin/organizations/update_urls_spec.rb b/spec/features/admin/organizations/update_urls_spec.rb new file mode 100644 index 000000000..2fb801e20 --- /dev/null +++ b/spec/features/admin/organizations/update_urls_spec.rb @@ -0,0 +1,46 @@ +require 'rails_helper' + +feature 'Update websites' do + background do + @org = create(:organization) + login_super_admin + visit '/admin/organizations/parent-agency' + end + + scenario 'when no websites exist' do + expect(page).to have_no_xpath("//input[@name='organization[urls][]']") + end + + scenario 'by adding 2 new websites', :js do + add_two_urls + expect(find_field('organization_urls_0').value).to eq 'http://ruby.com' + delete_all_urls + expect(page).to have_no_xpath("//input[@name='organization[urls][]']") + end + + scenario 'with 2 urls but one is empty', :js do + @org.update!(urls: ['http://ruby.org']) + visit '/admin/organizations/parent-agency' + click_link 'Add a website' + click_button 'Save changes' + total_urls = all(:xpath, "//input[@type='url']") + expect(total_urls.length).to eq 1 + end + + scenario 'with invalid website' do + @org.update!(urls: ['http://ruby.org']) + visit '/admin/organizations/parent-agency' + fill_in 'organization_urls_0', with: 'www.monfresh.com' + click_button 'Save changes' + expect(page).to have_content 'www.monfresh.com is not a valid URL' + end + + scenario 'with valid website' do + @org.update!(urls: ['http://ruby.org']) + visit '/admin/organizations/parent-agency' + fill_in 'organization_urls_0', with: 'http://codeforamerica.org' + click_button 'Save changes' + expect(find_field('organization_urls_0').value). + to eq 'http://codeforamerica.org' + end +end diff --git a/spec/features/admin/organizations/visit_organization_spec.rb b/spec/features/admin/organizations/visit_organization_spec.rb new file mode 100644 index 000000000..192b62655 --- /dev/null +++ b/spec/features/admin/organizations/visit_organization_spec.rb @@ -0,0 +1,74 @@ +require 'rails_helper' + +feature 'Visiting a specific organization' do + before(:all) do + @location = create(:location) + @organization = @location.organization + end + + before(:each) do + @organization.reload + @location.reload + end + + after(:all) do + Organization.find_each(&:destroy) + end + + scenario 'when admin has unmatched generic email' do + admin = create(:admin_with_generic_email) + login_as_admin(admin) + visit('/admin/organizations/parent-agency') + expect(page).to have_content "Sorry, you don't have access to that page" + expect(current_path).to eq(admin_dashboard_path) + end + + scenario 'when admin has unmatched custom domain name' do + login_admin + visit('/admin/organizations/parent-agency') + expect(page).to have_content "Sorry, you don't have access to that page" + expect(current_path).to eq(admin_dashboard_path) + end + + scenario 'when admin has matched custom domain name' do + @location.update!(urls: ['http://samaritanhouse.com']) + login_admin + visit('/admin/organizations/parent-agency') + expect(page).to_not have_content "Sorry, you don't have access to that page" + @location.update!(urls: []) + end + + scenario 'when admin is location admin' do + new_admin = create(:admin_with_generic_email) + @location.update!(admin_emails: [new_admin.email]) + login_as_admin(new_admin) + visit('/admin/organizations/parent-agency') + expect(page).to_not have_content "Sorry, you don't have access to that page" + @location.update!(admin_emails: []) + end + + scenario 'when admin is location admin but has non-generic email' do + login_admin + @location.update!(admin_emails: [@admin.email]) + visit('/admin/organizations/parent-agency') + expect(page).to_not have_content "Sorry, you don't have access to that page" + @location.update!(admin_emails: []) + end + + scenario 'when admin is super admin' do + login_super_admin + visit('/admin/organizations/parent-agency') + expect(page).to_not have_content "Sorry, you don't have access to that page" + end + + context 'when admin is not super admin' do + it 'denies access to create a new organization' do + login_admin + visit('/admin/organizations/new') + expect(page).to have_content "Sorry, you don't have access to that page" + expect(current_path).to eq(admin_dashboard_path) + visit('/admin/organizations') + expect(page).to_not have_link 'Add a new organization' + end + end +end diff --git a/spec/features/admin/organizations/visit_organizations_spec.rb b/spec/features/admin/organizations/visit_organizations_spec.rb new file mode 100644 index 000000000..229b443af --- /dev/null +++ b/spec/features/admin/organizations/visit_organizations_spec.rb @@ -0,0 +1,120 @@ +require 'rails_helper' + +feature 'Organizations page' do + context 'when not signed in' do + before :each do + visit '/admin/organizations' + end + + it 'redirects to the admin sign in page' do + expect(current_path).to eq(new_admin_session_path) + end + + it 'prompts the user to sign in or sign up' do + expect(page). + to have_content 'You need to sign in or sign up before continuing.' + end + + it 'includes a link to the sign in page in the navigation' do + within '.navbar' do + expect(page).to have_link 'Sign in', href: new_admin_session_path + end + end + + it 'includes a link to the sign up page in the navigation' do + within '.navbar' do + expect(page).to have_link 'Sign up', href: new_admin_registration_path + end + end + + it 'does not include a link to the Home page in the navigation' do + within '.navbar' do + expect(page).not_to have_link 'Home', href: root_path + end + end + + it 'does not include a link to Your organizations in the navigation' do + within '.navbar' do + expect(page).not_to have_link 'Your organizations', href: admin_organizations_path + end + end + end + + context 'when signed in' do + before :each do + login_admin + visit '/admin/organizations' + end + + it 'displays instructions for editing organizations' do + expect(page).to have_content 'Below you should see a list of organizations' + expect(page).to have_content 'To start updating, click on one of the links' + expect(page).not_to have_content 'As a super admin' + end + + it 'only shows links that belong to the admin' do + create(:nearby_loc) + create(:location_for_org_admin) + visit '/admin/organizations' + expect(page).not_to have_link 'Food Stamps' + expect(page).to have_link 'Parent Agency' + end + + it 'does not include a link to the sign up page in the navigation' do + within '.navbar' do + expect(page).not_to have_link 'Sign up' + end + end + + it 'does not include a link to the sign in page in the navigation' do + within '.navbar' do + expect(page).not_to have_link 'Sign in' + end + end + + it 'includes a link to sign out in the navigation' do + within '.navbar' do + expect(page). + to have_link 'Sign out', href: destroy_admin_session_path + end + end + + it 'includes a link to the Edit Account page in the navigation' do + within '.navbar' do + expect(page). + to have_link 'Edit account', href: edit_admin_registration_path + end + end + + it 'displays the name of the logged in admin in the navigation' do + within '.navbar' do + expect(page).to have_content "Logged in as #{@admin.name}" + end + end + + it 'includes a link to Your organizations in the navigation' do + within '.navbar' do + expect(page).to have_link 'Your organizations', href: admin_organizations_path + end + end + end + + context 'when signed in as super admin' do + before :each do + login_super_admin + visit '/admin/organizations' + end + + it 'displays instructions for editing organizations' do + expect(page).to have_content 'As a super admin' + end + + it 'shows all organizations' do + create(:nearby_loc) + create(:location_for_org_admin) + visit '/admin/organizations' + expect(page).to have_link 'Food Stamps' + expect(page).to have_link 'Parent Agency' + end + end +end diff --git a/spec/features/admin/sign_in_spec.rb b/spec/features/admin/sign_in_spec.rb index c4123afcb..97e19a261 100644 --- a/spec/features/admin/sign_in_spec.rb +++ b/spec/features/admin/sign_in_spec.rb @@ -37,19 +37,8 @@ sign_in_admin(valid_admin.email, valid_admin.password) end - it 'sets the current path to the admin locations path' do - expect(current_path).to eq(admin_locations_path) - end - - it "displays the admin's locations" do - create(:location_for_org_admin) - visit '/admin/locations' - expect(page).to have_content 'Below you should see a list' - expect(page).to have_content 'Parent Agency locations' - end - - it 'greets the admin by their name' do - expect(page).to have_content 'Welcome back, Org Admin!' + it 'sets the current path to the admin root path' do + expect(current_path).to eq(admin_dashboard_path) end it 'displays a success message' do diff --git a/spec/support/features/form_helpers.rb b/spec/support/features/form_helpers.rb index 2cc0d8745..5e7d8ff40 100644 --- a/spec/support/features/form_helpers.rb +++ b/spec/support/features/form_helpers.rb @@ -44,7 +44,6 @@ def update_mailing_address(options = {}) fill_in 'location_mail_address_attributes_city', with: options[:city] fill_in 'location_mail_address_attributes_state', with: options[:state] fill_in 'location_mail_address_attributes_zip', with: options[:zip] - click_button 'Save changes' end def remove_street_address @@ -148,9 +147,9 @@ def delete_all_admins def add_two_urls click_link 'Add a website' - fill_in 'location[urls][]', with: 'http://ruby.com' + fill_in find(:xpath, "//input[@type='url']")[:id], with: 'http://ruby.com' click_link 'Add a website' - urls = all(:xpath, "//input[@name='location[urls][]']") + urls = all(:xpath, "//input[@type='url']") fill_in urls[-1][:id], with: 'http://monfresh.com' click_button 'Save changes' end @@ -160,5 +159,16 @@ def delete_all_urls find_link('Delete this website permanently', match: :first).click click_button 'Save changes' end + + def fill_in_all_required_fields + select 'Parent Agency', from: 'location_organization_id' + fill_in 'location_name', with: 'New Parent Agency location' + fill_in 'location_description', with: 'new description' + click_link 'Add a street address' + fill_in 'location_address_attributes_street', with: '123 Main St.' + fill_in 'location_address_attributes_city', with: 'Belmont' + fill_in 'location_address_attributes_state', with: 'CA' + fill_in 'location_address_attributes_zip', with: '12345' + end end end From 9b26326a1bbe62a1825b432005f9e9f649d008cd Mon Sep 17 00:00:00 2001 From: Moncef Belyamani Date: Tue, 15 Jul 2014 01:22:22 -0400 Subject: [PATCH 15/17] Add CRUD functionality for Services. Closes #170. --- README.md | 23 +-- .../javascripts/admin/categories_form.js | 78 ++++++++++ app/assets/javascripts/form.js.coffee | 6 - app/assets/stylesheets/application.css.scss | 17 ++- app/controllers/admin/services_controller.rb | 81 +++++++++++ app/controllers/api/v1/services_controller.rb | 3 +- app/helpers/admin/form_helper.rb | 19 +++ app/models/location.rb | 7 +- app/models/organization.rb | 3 +- app/models/service.rb | 20 ++- app/views/admin/locations/_form.html.haml | 10 ++ .../admin/organizations/forms/_urls.html.haml | 2 +- .../_confirm_delete_service.html.haml | 15 ++ app/views/admin/services/_form.html.haml | 37 +++++ .../services/confirm_delete_service.js.erb | 1 + app/views/admin/services/edit.html.haml | 5 + .../admin/services/forms/_audience.html.haml | 7 + .../services/forms/_categories.html.haml | 9 ++ .../services/forms/_description.html.haml | 7 + .../services/forms/_eligibility.html.haml | 7 + .../admin/services/forms/_fees.html.haml | 7 + .../services/forms/_how_to_apply.html.haml | 7 + .../services/forms/_keyword_fields.html.haml | 4 + .../admin/services/forms/_keywords.html.haml | 15 ++ .../admin/services/forms/_name.html.haml | 7 + .../forms/_new_service_form.html.haml | 26 ++++ .../forms/_service_area_fields.html.haml | 4 + .../services/forms/_service_areas.html.haml | 17 +++ .../services/forms/_url_fields.html.haml | 4 + .../admin/services/forms/_urls.html.haml | 16 ++ .../admin/services/forms/_wait.html.haml | 7 + app/views/admin/services/new.html.haml | 5 + config/routes.rb | 11 +- data/sample_data.json | 14 +- spec/api/create_service_spec.rb | 4 +- spec/api/get_location_services_spec.rb | 6 +- spec/factories/services.rb | 8 +- .../admin/services/create_service_spec.rb | 137 ++++++++++++++++++ .../admin/services/update_audience_spec.rb | 17 +++ .../admin/services/update_categories_spec.rb | 43 ++++++ .../admin/services/update_description_spec.rb | 23 +++ .../admin/services/update_eligibility_spec.rb | 17 +++ .../admin/services/update_fees_spec.rb | 17 +++ .../services/update_how_to_apply_spec.rb | 17 +++ .../admin/services/update_keywords_spec.rb | 45 ++++++ .../admin/services/update_name_spec.rb | 23 +++ .../services/update_service_areas_spec.rb | 60 ++++++++ .../admin/services/update_urls_spec.rb | 50 +++++++ .../admin/services/update_wait_time_spec.rb | 17 +++ spec/models/service_spec.rb | 17 +-- spec/support/features/form_helpers.rb | 32 ++++ 51 files changed, 980 insertions(+), 54 deletions(-) create mode 100644 app/assets/javascripts/admin/categories_form.js create mode 100644 app/controllers/admin/services_controller.rb create mode 100644 app/views/admin/services/_confirm_delete_service.html.haml create mode 100644 app/views/admin/services/_form.html.haml create mode 100644 app/views/admin/services/confirm_delete_service.js.erb create mode 100644 app/views/admin/services/edit.html.haml create mode 100644 app/views/admin/services/forms/_audience.html.haml create mode 100644 app/views/admin/services/forms/_categories.html.haml create mode 100644 app/views/admin/services/forms/_description.html.haml create mode 100644 app/views/admin/services/forms/_eligibility.html.haml create mode 100644 app/views/admin/services/forms/_fees.html.haml create mode 100644 app/views/admin/services/forms/_how_to_apply.html.haml create mode 100644 app/views/admin/services/forms/_keyword_fields.html.haml create mode 100644 app/views/admin/services/forms/_keywords.html.haml create mode 100644 app/views/admin/services/forms/_name.html.haml create mode 100644 app/views/admin/services/forms/_new_service_form.html.haml create mode 100644 app/views/admin/services/forms/_service_area_fields.html.haml create mode 100644 app/views/admin/services/forms/_service_areas.html.haml create mode 100644 app/views/admin/services/forms/_url_fields.html.haml create mode 100644 app/views/admin/services/forms/_urls.html.haml create mode 100644 app/views/admin/services/forms/_wait.html.haml create mode 100644 app/views/admin/services/new.html.haml create mode 100644 spec/features/admin/services/create_service_spec.rb create mode 100644 spec/features/admin/services/update_audience_spec.rb create mode 100644 spec/features/admin/services/update_categories_spec.rb create mode 100644 spec/features/admin/services/update_description_spec.rb create mode 100644 spec/features/admin/services/update_eligibility_spec.rb create mode 100644 spec/features/admin/services/update_fees_spec.rb create mode 100644 spec/features/admin/services/update_how_to_apply_spec.rb create mode 100644 spec/features/admin/services/update_keywords_spec.rb create mode 100644 spec/features/admin/services/update_name_spec.rb create mode 100644 spec/features/admin/services/update_service_areas_spec.rb create mode 100644 spec/features/admin/services/update_urls_spec.rb create mode 100644 spec/features/admin/services/update_wait_time_spec.rb diff --git a/README.md b/README.md index 3f3ce1413..4f1b99f5c 100644 --- a/README.md +++ b/README.md @@ -2,26 +2,33 @@ [![Stories in Ready](https://badge.waffle.io/codeforamerica/ohana-api.png?label=ready)](https://waffle.io/codeforamerica/ohana-api) [![Build Status](https://travis-ci.org/codeforamerica/ohana-api.png?branch=master)](https://travis-ci.org/codeforamerica/ohana-api) -This is the API portion of the [Ohana API](http://ohanapi.org) project, an open source community resource platform developed by [@monfresh](https://github.com/monfresh), [@spara](https://github.com/spara), and [@anselmbradford](https://github.com/anselmbradford) during their Code for America Fellowship in 2013, in partnership with San Mateo County's Human Services Agency. The goal of the project is to make it easier for residents in need to find services they are eligible for. +This is the API + Admin Interface portion of the [Ohana API](http://ohanapi.org) project, an open source community resource platform developed by [@monfresh](https://github.com/monfresh), [@spara](https://github.com/spara), and [@anselmbradford](https://github.com/anselmbradford) during their Code for America Fellowship in 2013, in partnership with San Mateo County's Human Services Agency. Ohana makes it easy for communities to publish a database of social services, and allows developers to build impactful applications that serve underprivileged residents. -Before we started working on the Ohana API, the search interface that residents and social workers in San Mateo County had access to was the Peninsula Library System's [CIP portal](http://catalog.plsinfo.org:81/). As a demonstration of the kind of applications that can be built on top of the Ohana API, we developed a [better search interface](http://smc-connect.org) ([repo link](https://github.com/codeforamerica/human_services_finder)) that consumes the API via our [Ruby wrapper](https://github.com/codeforamerica/ohanakapa). We also built an [admin site](https://github.com/codeforamerica/ohana-api-admin) to allow organizations to update their own information. +Before we started working on the Ohana API, the search interface that residents and social workers in San Mateo County had access to was the Peninsula Library System's [CIP portal](http://catalog.plsinfo.org:81/). As a demonstration of the kind of applications that can be built on top of the Ohana API, we developed a [better search interface](http://smc-connect.org) ([repo link](https://github.com/codeforamerica/ohana-web-search)) that consumes the API via our [Ruby wrapper](https://github.com/codeforamerica/ohanakapa). ## Stack Overview * Ruby version 2.1.1 -* Rails version 4.1.1 +* Rails version 4.1.4 * Postgres * Testing Frameworks: RSpec, Factory Girl, Capybara ## Demo -You can see a running version of the application at -[http://ohana-api-demo.herokuapp.com/](http://ohana-api-demo.herokuapp.com/). +You can see a running version of the different parts of the application here: + +**Developer portal**: [http://ohana-api-demo.herokuapp.com/](http://ohana-api-demo.herokuapp.com/) (see [db/seeds.rb][seeds] for a list of usernames and passwords you can sign in with.) + +**API**: [http://ohana-api-demo.herokuapp.com/api](http://ohana-api-demo.herokuapp.com/api) + +**Admin Interface**: [http://ohana-api-demo.herokuapp.com/admin](http://ohana-api-demo.herokuapp.com/admin) (see [db/seeds.rb][seeds] for a list of usernames and passwords you can sign in with.) + +[seeds]: https://github.com/codeforamerica/ohana-api/blob/master/db/seeds.rb ## Current Status We are happy to announce that this project has been awarded a [grant from the Knight Foundation](http://www.knightfoundation.org/grants/201447979/), which means we get to keep working on it in 2014! Our primary goals this year are: simplifying the installation process, streamlining the code, reducing dependencies, and preparing the project for broader installation by a variety of organizations and governments. ## Data Schema -If you would like to try out the current version of the project that uses Postgres, please read the Wiki article about [Populating the Postgres DB from a JSON file](https://github.com/codeforamerica/ohana-api/wiki/Populating-the-Postgres-database-from-a-JSON-file). That article documents the current schema and data dictionary, but please note that this will be in flux as we are working with various interested parties to define a [Human Services Data Specification](https://github.com/codeforamerica/OpenReferral). +If you would like to try out the current version of the project, please read the Wiki article about [Populating the Postgres DB from a JSON file](https://github.com/codeforamerica/ohana-api/wiki/Populating-the-Postgres-database-from-a-JSON-file). That article documents the current schema and data dictionary, but please note that this will be in flux as we are working with various interested parties to define a [Human Services Data Specification](https://github.com/codeforamerica/OpenReferral). ## API documentation [http://ohanapi.herokuapp.com/api/docs](http://ohana-api-demo.herokuapp.com/api/docs) @@ -41,12 +48,10 @@ If you've built one, let us know and we'll add it here. By default, this project uses the [Open Eligibility](http://openeligibility.org) taxonomy to assign Services to [Categories](https://github.com/codeforamerica/ohana-api/blob/master/app/models/category.rb). If you would like to use your own taxonomy, feel free to update this rake task to [create your own hierarchy or tree structure](https://github.com/codeforamerica/ohana-api/blob/master/lib/tasks/oe.rake). Then run `rake create_categories`. -The easiest way to assign categories to a service is to use the [Ohana API Admin](https://github.com/codeforamerica/ohana-api-admin/blob/master/app/controllers/hsa_controller.rb#L183-187) interface. Here's a screenshot: +The easiest way to assign categories to a service is to use the Admin interface. Here's a screenshot: ![Editing categories in Ohana API Admin](https://github.com/codeforamerica/ohana-api/raw/master/categories-in-ohana-api-admin.png) -You can also try it from the Rails console, mimicking how the API would do it when it receives a [PUT request to update a service's categories](https://github.com/codeforamerica/ohana-api/blob/master/app/api/ohana.rb#L239-257). - ## Apps that are using the Ohana API [SMC-Connect](http://www.smc-connect.org) [GitHub repo for SMC-Connect](https://github.com/codeforamerica/human_services_finder) diff --git a/app/assets/javascripts/admin/categories_form.js b/app/assets/javascripts/admin/categories_form.js new file mode 100644 index 000000000..f67a9c062 --- /dev/null +++ b/app/assets/javascripts/admin/categories_form.js @@ -0,0 +1,78 @@ +var main = (function () { +"use strict"; + + var NUM_LEVELS = 2; + + // initalize the application + function init() + { + var checkboxes = $('#categories input'); + + var currentCheckbox; + for (var i=0; i < checkboxes.length; i++) + { + currentCheckbox = checkboxes[i]; + _checkState('depth',0,currentCheckbox); + } + + var lnks = $('#categories input'); + + var curr; + for (var l=0; l < lnks.length; l++) + { + curr = lnks[l]; + $(curr).click(_linkClickedHandler) + } + } + + function _linkClickedHandler(evt) + { + var el = evt.target; + if (el.nodeName == 'INPUT') + { + _checkState('depth',0,el); + } + + } + + function _checkState(prefix,depth,checkbox) + { + var item = $(checkbox).parent(); // parent li item + var id = prefix+String(depth); + while(!item.hasClass(id)) + { + depth++; + id = prefix+String(depth); + } + + id = 'li.'+prefix+String(depth+1); + var lnks = $(id,item); + var curr; + for (var l=0; l < lnks.length; l++) + { + curr = lnks[l]; + if (checkbox.checked) + { + $(curr).removeClass('hide'); + } + else + { + $(curr).addClass('hide'); + checkbox = $('input',$(curr)) + checkbox.prop('checked', false); + _checkState(prefix,depth,checkbox) + } + } + + } + + // return internally scoped var as value of globally scoped object + return { + init:init + }; + +})(); + +$(document).ready(function(){ + main.init(); +}); diff --git a/app/assets/javascripts/form.js.coffee b/app/assets/javascripts/form.js.coffee index eb65a616e..1ba354d92 100644 --- a/app/assets/javascripts/form.js.coffee +++ b/app/assets/javascripts/form.js.coffee @@ -21,9 +21,3 @@ jQuery -> inputs = $(this).parent().find('input') inputs[inputs.length - 1].setAttribute('id', time) event.preventDefault() - - $('.new_entry').on 'click', '.add_fields', (event) -> - time = new Date().getTime() - regexp = new RegExp($(this).data('id'), 'g') - $(this).before($(this).data('fields').replace(regexp, time)) - event.preventDefault() \ No newline at end of file diff --git a/app/assets/stylesheets/application.css.scss b/app/assets/stylesheets/application.css.scss index 38c8838ad..9209212a8 100644 --- a/app/assets/stylesheets/application.css.scss +++ b/app/assets/stylesheets/application.css.scss @@ -51,6 +51,7 @@ padding:0; text-transform: uppercase; } + p { padding-top: 10px; } } .inst-box @@ -79,9 +80,9 @@ } label {font-weight: bold; font-size:16px;} } - input + input, textarea { - margin:0px 10px 20px 0px; + margin:0px 0px 20px 0px; } p @@ -99,14 +100,22 @@ margin: 10px 0px 15px 10px; } -#categories, #accessibility +#accessibility { ul {list-style-type: none; margin-left:15px;} li {vertical-align:middle;} - input {height:20px;width:20px;margin-right:5px;margin-bottom:0px;display:inline-block;} + input {height:20px;width:20px;margin-right:5px;margin-bottom:0px;margin-top:3px;display:inline-block;} label {display:inline-block;vertical-align:middle;width:220px;margin-top:7px;} } +#categories +{ + ul {list-style-type: none; margin-left:15px;} + li {vertical-align:middle;} + input {height:20px;width:20px;margin:5px 5px 5px 0px;display:inline-block;} + label {display:inline-block;vertical-align:middle;width:220px;} +} + // margin added to accommodate for floating footer // This needs to be added to the last form element before the save button .edit_entry, .new_entry diff --git a/app/controllers/admin/services_controller.rb b/app/controllers/admin/services_controller.rb new file mode 100644 index 000000000..cb59ca86b --- /dev/null +++ b/app/controllers/admin/services_controller.rb @@ -0,0 +1,81 @@ +class Admin + class ServicesController < ApplicationController + before_action :authenticate_admin! + layout 'admin' + + def edit + @location = Location.find(params[:location_id]) + @service = Service.find(params[:id]) + @admin_decorator = AdminDecorator.new(current_admin) + @oe_ids = @service.categories.pluck(:oe_id) + + unless @admin_decorator.allowed_to_access_location?(@location) + redirect_to admin_dashboard_path, + alert: "Sorry, you don't have access to that page." + end + end + + def update + @service = Service.find(params[:id]) + @location = Location.find(params[:location_id]) + + respond_to do |format| + if @service.update(params[:service]) + format.html do + redirect_to [:admin, @location, @service], + notice: 'Service was successfully updated.' + end + else + format.html { render :edit } + end + end + end + + def new + @admin_decorator = AdminDecorator.new(current_admin) + @location = Location.find(params[:location_id]) + @oe_ids = [] + + unless @admin_decorator.allowed_to_access_location?(@location) + redirect_to admin_dashboard_path, + alert: "Sorry, you don't have access to that page." + end + + @service = Service.new + end + + def create + @service = Service.new(params[:service]) + @location = Location.find(params[:location_id]) + @oe_ids = [] + + respond_to do |format| + if @service.save + format.html do + redirect_to admin_location_path(@location), + notice: "Service '#{@service.name}' was successfully created." + end + else + format.html { render :new } + end + end + end + + def destroy + service = Service.find(params[:id]) + service.destroy + respond_to do |format| + format.html { redirect_to admin_locations_path } + end + end + + def confirm_delete_service + @service_name = params[:service_name] + @service_id = params[:service_id] + respond_to do |format| + format.html + format.js + end + end + end +end diff --git a/app/controllers/api/v1/services_controller.rb b/app/controllers/api/v1/services_controller.rb index 986aaeb19..de12e1f92 100644 --- a/app/controllers/api/v1/services_controller.rb +++ b/app/controllers/api/v1/services_controller.rb @@ -14,7 +14,8 @@ def index def update service = Service.find(params[:id]) - service.update!(params) + location = Location.find(params[:location_id]) + service.update!(params.merge(location_id: location.id)) render json: service, status: 200 end diff --git a/app/helpers/admin/form_helper.rb b/app/helpers/admin/form_helper.rb index a44b8001d..1cd2a003b 100644 --- a/app/helpers/admin/form_helper.rb +++ b/app/helpers/admin/form_helper.rb @@ -14,5 +14,24 @@ def link_to_add_array_fields(name, model, field) fields = render("admin/#{model}/forms/#{field}_fields") link_to(name, '#', class: 'add_array_fields btn btn-primary', data: { id: id, fields: fields.gsub('\n', '') }) end + + def nested_categories(categories) + cats = [] + categories.each do |array| + cats.push([array.first, array.second]) + end + + cats.map do |category, sub_categories| + class_name = category.depth == 0 ? 'depth0' : "hide depth#{category.depth}" + + content_tag(:ul) do + concat(content_tag(:li, class: class_name) do + concat(check_box_tag 'service[category_ids][]', category.id, @oe_ids.include?(category.oe_id), id: "category_#{category.oe_id}") + concat(label_tag dom_id(category), category.name) + concat(nested_categories(sub_categories)) + end) + end + end.join.html_safe + end end end diff --git a/app/models/location.rb b/app/models/location.rb index dc7c46c03..21d8fa7bc 100644 --- a/app/models/location.rb +++ b/app/models/location.rb @@ -111,11 +111,12 @@ class Location < ActiveRecord::Base auto_strip_attributes :description, :hours, :name, :short_desc, :transportation, squish: true - before_save :compact_array_fields + before_save :compact_and_squish_array_fields - def compact_array_fields + def compact_and_squish_array_fields %w(admin_emails emails urls).each do |name| - send("#{name}=", send(name).reject(&:blank?)) if send(name).is_a?(Array) + return unless send(name).is_a?(Array) + send("#{name}=", send(name).reject(&:blank?).map(&:squish)) end end diff --git a/app/models/organization.rb b/app/models/organization.rb index 5ce69ac69..3e02844e7 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -23,7 +23,8 @@ class Organization < ActiveRecord::Base before_save :compact_urls def compact_urls - send('urls=', send('urls').reject(&:blank?)) if send('urls').is_a?(Array) + return unless send('urls').is_a?(Array) + send('urls=', send('urls').reject(&:blank?).map(&:squish)) end extend FriendlyId diff --git a/app/models/service.rb b/app/models/service.rb index a11034cf1..1da95ab3e 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -1,13 +1,19 @@ class Service < ActiveRecord::Base attr_accessible :audience, :description, :eligibility, :fees, - :funding_sources, :keywords, :how_to_apply, :name, - :service_areas, :short_desc, :urls, :wait + :funding_sources, :how_to_apply, :keywords, :name, + :service_areas, :short_desc, :urls, :wait, :category_ids, + :location_id belongs_to :location, touch: true + has_and_belongs_to_many :categories, -> { order('oe_id asc').uniq } + # has_many :schedules # accepts_nested_attributes_for :schedules + validates :name, :description, :location, + presence: { message: "can't be blank for Service" } + validates :urls, array: { format: { with: %r{\Ahttps?://([^\s:@]+:[^\s:@]*@)?[A-Za-z\d\-]+(\.[A-Za-z\d\-]+)+\.?(:\d{1,5})?([\/?]\S*)?\z}i, message: '%{value} is not a valid URL' } } @@ -19,7 +25,7 @@ class Service < ActiveRecord::Base auto_strip_attributes :audience, :description, :eligibility, :fees, :how_to_apply, :name, :short_desc, :wait, squish: true - before_validation :strip_whitespace_from_each_keyword + before_validation :compact_and_squish_array_fields serialize :funding_sources, Array serialize :keywords, Array @@ -36,8 +42,10 @@ def service_area_format errors.add(:service_areas, error_message) end - def strip_whitespace_from_each_keyword - return unless keywords.is_a?(Array) - self.keywords = keywords.map(&:squish) + def compact_and_squish_array_fields + %w(funding_sources keywords service_areas urls).each do |name| + return unless send(name).is_a?(Array) + send("#{name}=", send(name).reject(&:blank?).map(&:squish)) + end end end diff --git a/app/views/admin/locations/_form.html.haml b/app/views/admin/locations/_form.html.haml index 8c3005d71..5583c00e0 100644 --- a/app/views/admin/locations/_form.html.haml +++ b/app/views/admin/locations/_form.html.haml @@ -21,6 +21,16 @@ = render 'admin/locations/forms/urls', f: f = render 'admin/locations/forms/accessibility', f: f +%div.content-box + %h2= 'Services' + = 'Click a Service below to view and update it:' + %p + - @location.services.each_with_index do |service, i| + = link_to service.name || "Service ##{i+1}", edit_admin_location_service_path(@location, service) + %br + %p + = link_to 'Add a new service', new_admin_location_service_path(@location), class: 'btn btn-primary' + %div.danger-zone %header %strong diff --git a/app/views/admin/organizations/forms/_urls.html.haml b/app/views/admin/organizations/forms/_urls.html.haml index 9aaa1360e..91173f404 100644 --- a/app/views/admin/organizations/forms/_urls.html.haml +++ b/app/views/admin/organizations/forms/_urls.html.haml @@ -11,6 +11,6 @@ = field_set_tag do = url_field_tag 'organization[urls][]', url, class: 'span9', id: "organization_urls_#{i}" %br - = link_to "Delete this website permanently", '#', class: "btn btn-danger delete_attribute" + = link_to 'Delete this website permanently', '#', class: 'btn btn-danger delete_attribute' = link_to_add_array_fields 'Add a website', :organizations, :url diff --git a/app/views/admin/services/_confirm_delete_service.html.haml b/app/views/admin/services/_confirm_delete_service.html.haml new file mode 100644 index 000000000..755fb2ae1 --- /dev/null +++ b/app/views/admin/services/_confirm_delete_service.html.haml @@ -0,0 +1,15 @@ +%div.modal-header + %button.close{'aria-hidden' => 'true', 'data-dismiss' => 'modal', 'type' => 'button'} × + %h3#myModalLabel Are you ABSOLUTELY sure? +%div.modal-body + %p + = 'This action CANNOT be undone. This will delete ' + %strong + ="#{@service_name || @service_id}." + / %p + / Please type in the name of the service to confirm. + / %p + / = text_field_tag "service-name", "", class: "span5" +%div.modal-footer + %button.btn{'aria-hidden' => 'true', 'data-dismiss' => 'modal'} Close + = link_to 'I understand the consequences, delete this service', { action: :destroy, id: @service_id, name: @service_name }, method: :delete, class: 'btn btn-danger' diff --git a/app/views/admin/services/_form.html.haml b/app/views/admin/services/_form.html.haml new file mode 100644 index 000000000..503c8c752 --- /dev/null +++ b/app/views/admin/services/_form.html.haml @@ -0,0 +1,37 @@ +- if @service.errors.any? + .alert.alert-danger + %h2= "#{pluralize(@service.errors.count, "error")} prohibited this service from being saved:" + %ul + - @service.errors.full_messages.each do |msg| + %li= msg + += render 'admin/services/forms/name', f: f += render 'admin/services/forms/description', f: f += render 'admin/services/forms/audience', f: f += render 'admin/services/forms/eligibility', f: f += render 'admin/services/forms/fees', f: f += render 'admin/services/forms/how_to_apply', f: f += render 'admin/services/forms/keywords', f: f += render 'admin/services/forms/service_areas', f: f += render 'admin/services/forms/urls', f: f += render 'admin/services/forms/wait', f: f += render 'admin/services/forms/categories', f: f + +%div.danger-zone + %header + %strong + Danger Zone + %h4 + Delete this service + %p + Once you delete a service, there is no going back. Please be certain. + %p + = link_to 'Permanently delete this service', { action: :confirm_delete_service, service_id: @service.id, service_name: @service.name }, remote: true, data: { toggle: 'modal', target: '#modal-window' }, class: 'boxed-action' +%div#modal-window.modal.hide.fade{'aria-hidden' => 'true', 'aria-labelledby' => 'myModalLabel', 'role' => 'dialog'} + +%div.save-box.navbar-inner + %p + = 'Editing' + %strong + = "#{@service.name}" + = f.submit 'Save changes & apply edits to database', class: 'btn btn-success', data: { disable_with: 'Please wait...' } diff --git a/app/views/admin/services/confirm_delete_service.js.erb b/app/views/admin/services/confirm_delete_service.js.erb new file mode 100644 index 000000000..20d7123f4 --- /dev/null +++ b/app/views/admin/services/confirm_delete_service.js.erb @@ -0,0 +1 @@ +$("#modal-window").html("<%= escape_javascript(render 'admin/services/confirm_delete_service') %>"); diff --git a/app/views/admin/services/edit.html.haml b/app/views/admin/services/edit.html.haml new file mode 100644 index 000000000..8bf04c930 --- /dev/null +++ b/app/views/admin/services/edit.html.haml @@ -0,0 +1,5 @@ +%div.content-box + %h2= "#{@service.try(:name)} / #{@location.name}" += form_for [:admin, @location, @service], html: { class: 'edit_entry' } do |f| + = render 'admin/services/form', f: f + diff --git a/app/views/admin/services/forms/_audience.html.haml b/app/views/admin/services/forms/_audience.html.haml new file mode 100644 index 000000000..02083507d --- /dev/null +++ b/app/views/admin/services/forms/_audience.html.haml @@ -0,0 +1,7 @@ +%div.inst-box + %header + = f.label :audience, 'Audience' + %p.desc + What groups are served, if not everyone? + %p + = f.text_area :audience, class: 'span6' diff --git a/app/views/admin/services/forms/_categories.html.haml b/app/views/admin/services/forms/_categories.html.haml new file mode 100644 index 000000000..60c4f906a --- /dev/null +++ b/app/views/admin/services/forms/_categories.html.haml @@ -0,0 +1,9 @@ +%div.inst-box + %header + %strong + Categories + %p.desc + What categories best describe this service? + = hidden_field_tag 'service[category_ids][]', nil + = field_set_tag nil, id: 'categories' do + = nested_categories(Category.arrange(order: :oe_id)) diff --git a/app/views/admin/services/forms/_description.html.haml b/app/views/admin/services/forms/_description.html.haml new file mode 100644 index 000000000..3a09a5b34 --- /dev/null +++ b/app/views/admin/services/forms/_description.html.haml @@ -0,0 +1,7 @@ +%div.inst-box + %header + = f.label :description, 'Description' + %span.desc + A description of the service. + %p + = f.text_area :description, required: true, class: 'span9', rows: 5 diff --git a/app/views/admin/services/forms/_eligibility.html.haml b/app/views/admin/services/forms/_eligibility.html.haml new file mode 100644 index 000000000..fa7fefb9d --- /dev/null +++ b/app/views/admin/services/forms/_eligibility.html.haml @@ -0,0 +1,7 @@ +%div.inst-box + %header + = f.label :eligibility, 'Eligibility' + %p.desc + What criteria must served groups meet to receive service? + %p + = f.text_area :eligibility, class: 'span6' diff --git a/app/views/admin/services/forms/_fees.html.haml b/app/views/admin/services/forms/_fees.html.haml new file mode 100644 index 000000000..6ccdc361f --- /dev/null +++ b/app/views/admin/services/forms/_fees.html.haml @@ -0,0 +1,7 @@ +%div.inst-box + %header + = f.label :fees, 'Fees' + %p.desc + Are there any fees to receive this service? + %p + = f.text_area :fees, class: 'span5' diff --git a/app/views/admin/services/forms/_how_to_apply.html.haml b/app/views/admin/services/forms/_how_to_apply.html.haml new file mode 100644 index 000000000..4dc9545e1 --- /dev/null +++ b/app/views/admin/services/forms/_how_to_apply.html.haml @@ -0,0 +1,7 @@ +%div.inst-box + %header + = f.label :how_to_apply, 'How to apply' + %p.desc + How does a client apply to receive services, if applicable? + %p + = f.text_area :how_to_apply, class: 'span5' diff --git a/app/views/admin/services/forms/_keyword_fields.html.haml b/app/views/admin/services/forms/_keyword_fields.html.haml new file mode 100644 index 000000000..af4e59cee --- /dev/null +++ b/app/views/admin/services/forms/_keyword_fields.html.haml @@ -0,0 +1,4 @@ += field_set_tag do + = text_field_tag 'service[keywords][]', '', class: 'span4' + %br + = link_to "Delete this keyword permanently", '#', class: "btn btn-danger delete_attribute" diff --git a/app/views/admin/services/forms/_keywords.html.haml b/app/views/admin/services/forms/_keywords.html.haml new file mode 100644 index 000000000..b0e112f66 --- /dev/null +++ b/app/views/admin/services/forms/_keywords.html.haml @@ -0,0 +1,15 @@ +%div.inst-box + %header + %strong + Keywords + %p.desc + What search terms does this location's services fall under? + + - if @service.keywords.present? + - @service.keywords.each_with_index do |keyword, i| + = field_set_tag do + = text_field_tag 'service[keywords][]', keyword, class: 'span4', id: "service_keywords_#{i}" + %br + = link_to 'Delete this keyword permanently', '#', class: 'btn btn-danger delete_attribute' + = link_to_add_array_fields 'Add a keyword', :services, :keyword + diff --git a/app/views/admin/services/forms/_name.html.haml b/app/views/admin/services/forms/_name.html.haml new file mode 100644 index 000000000..ec3981b13 --- /dev/null +++ b/app/views/admin/services/forms/_name.html.haml @@ -0,0 +1,7 @@ +%div.inst-box + %header + = f.label :name, 'Service Name' + %span.desc + The name of the service. + %p + = f.text_field :name, required: true, maxlength: 255, class: 'span10' diff --git a/app/views/admin/services/forms/_new_service_form.html.haml b/app/views/admin/services/forms/_new_service_form.html.haml new file mode 100644 index 000000000..e7fa57ffe --- /dev/null +++ b/app/views/admin/services/forms/_new_service_form.html.haml @@ -0,0 +1,26 @@ +- if @service.errors.any? + .alert.alert-danger + %h2= "#{pluralize(@service.errors.count, "error")} prohibited this service from being saved:" + %ul + - @service.errors.full_messages.each do |msg| + %li= msg + += render 'admin/services/forms/name', f: f += render 'admin/services/forms/description', f: f += render 'admin/services/forms/audience', f: f += render 'admin/services/forms/eligibility', f: f += render 'admin/services/forms/fees', f: f += render 'admin/services/forms/how_to_apply', f: f += render 'admin/services/forms/keywords', f: f += render 'admin/services/forms/service_areas', f: f += render 'admin/services/forms/urls', f: f += render 'admin/services/forms/wait', f: f += render 'admin/services/forms/categories', f: f += hidden_field_tag 'service[location_id]', @location.id, id: 'service_location_id' + +%div.save-box.navbar-inner + %p + = 'Creating service for' + %strong + = "#{@location.name}" + = f.submit 'Create service', class: 'btn btn-success', data: { disable_with: 'Please wait...' } diff --git a/app/views/admin/services/forms/_service_area_fields.html.haml b/app/views/admin/services/forms/_service_area_fields.html.haml new file mode 100644 index 000000000..d5a24c6e6 --- /dev/null +++ b/app/views/admin/services/forms/_service_area_fields.html.haml @@ -0,0 +1,4 @@ += field_set_tag do + = text_field_tag 'service[service_areas][]', '', class: 'span4' + %br + = link_to "Delete this service area permanently", '#', class: "btn btn-danger delete_attribute" diff --git a/app/views/admin/services/forms/_service_areas.html.haml b/app/views/admin/services/forms/_service_areas.html.haml new file mode 100644 index 000000000..b8136876d --- /dev/null +++ b/app/views/admin/services/forms/_service_areas.html.haml @@ -0,0 +1,17 @@ +%div.inst-box + %header + %strong + Service Areas + %p.desc + What city or county does the location serve? + %em + Can either be city names or county names. Each word must be capitalized. + + - if @service.service_areas.present? + - @service.service_areas.each_with_index do |service_area, i| + = field_set_tag do + = text_field_tag 'service[service_areas][]', service_area, class: 'span4', id: "service_service_areas_#{i}" + %br + = link_to 'Delete this service area permanently', '#', class: 'btn btn-danger delete_attribute' + = link_to_add_array_fields 'Add a service area', :services, :service_area + diff --git a/app/views/admin/services/forms/_url_fields.html.haml b/app/views/admin/services/forms/_url_fields.html.haml new file mode 100644 index 000000000..4293062d2 --- /dev/null +++ b/app/views/admin/services/forms/_url_fields.html.haml @@ -0,0 +1,4 @@ += field_set_tag do + = url_field_tag 'service[urls][]', '', class: 'span9' + %br + = link_to "Delete this website permanently", '#', class: "btn btn-danger delete_attribute" diff --git a/app/views/admin/services/forms/_urls.html.haml b/app/views/admin/services/forms/_urls.html.haml new file mode 100644 index 000000000..a25cf66e9 --- /dev/null +++ b/app/views/admin/services/forms/_urls.html.haml @@ -0,0 +1,16 @@ +%div.inst-box.urls + %header + %strong + Websites + %p.desc + What websites are associated with the service? + %em + Must include "http://" or "https://" + - if @service.urls.present? + - @service.urls.each_with_index do |url, i| + = field_set_tag do + = url_field_tag 'service[urls][]', url, class: 'span9', id: "service_urls_#{i}" + %br + = link_to "Delete this website permanently", '#', class: "btn btn-danger delete_attribute" + = link_to_add_array_fields 'Add a website', :services, :url + diff --git a/app/views/admin/services/forms/_wait.html.haml b/app/views/admin/services/forms/_wait.html.haml new file mode 100644 index 000000000..8db1b6a6a --- /dev/null +++ b/app/views/admin/services/forms/_wait.html.haml @@ -0,0 +1,7 @@ +%div.inst-box + %header + = f.label :wait, 'Wait Time' + %p.desc + How long on average does a client need to wait to receive services? + %p + = f.text_area :wait, class: 'span5' diff --git a/app/views/admin/services/new.html.haml b/app/views/admin/services/new.html.haml new file mode 100644 index 000000000..6be3d700a --- /dev/null +++ b/app/views/admin/services/new.html.haml @@ -0,0 +1,5 @@ +%div.content-box + %h1 Create a new service + += form_for [:admin, @location, @service], html: { method: :post, class: 'edit_entry' } do |f| + = render 'admin/services/forms/new_service_form', f: f diff --git a/config/routes.rb b/config/routes.rb index 8017c8a37..e3ea0dcd8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -12,13 +12,22 @@ namespace :admin do root to: 'dashboard#index', as: :dashboard - resources :locations, except: :show + + resources :locations, except: :show do + resources :services, except: [:show, :index] + end + resources :organizations, except: :show + + get 'locations/:location_id/services/confirm_delete_service', to: 'services#confirm_delete_service', as: :confirm_delete_service get 'organizations/confirm_delete_organization', to: 'organizations#confirm_delete_organization', as: :confirm_delete_organization get 'locations/confirm_delete_location', to: 'locations#confirm_delete_location', as: :confirm_delete_location + + get 'locations/:location_id/services/:id', to: 'services#edit' get 'locations/:id', to: 'locations#edit' get 'organizations/:id', to: 'organizations#edit' end + devise_for :admins, path: 'admin', controllers: { registrations: 'admin/registrations' } resources :api_applications, except: :show diff --git a/data/sample_data.json b/data/sample_data.json index 4719d69f4..2efcac55c 100644 --- a/data/sample_data.json +++ b/data/sample_data.json @@ -1,7 +1,7 @@ -{"name":"Peninsula Family Service","locations":[{"name":"Fair Oaks Adult Activity Center","contacts_attributes":[{"name":"Susan Houston","title":"Director of Older Adult Services"},{"name":" Christina Gonzalez","title":"Center Director"}],"description":"A walk-in center for older adults that provides social services, wellness, recreational, educational and creative activities including arts and crafts, computer classes and gardening classes. Coffee and healthy breakfasts are available daily. A hot lunch is served Tuesday-Friday for persons age 60 or over and spouse. Provides case management (including in-home assessments) and bilingual information and referral about community services to persons age 60 or over on questions of housing, employment, household help, recreation and social activities, home delivered meals, health and counseling services and services to shut-ins. Health insurance and legal counseling is available by appointment. Lectures on a variety of health and fitness topics are held monthly in both English and Spanish. Provides a variety of physical fitness opportunities, including a garden club, yoga, tai chi, soul line dance and aerobics classes geared toward older adults. Also provides free monthly blood pressure screenings, quarterly blood glucose monitoring and health screenings by a visiting nurse. Offers a Brown Bag Program in which low-income seniors can receive a bag of groceries each week for a membership fee of $10 a year. Offers Spanish lessons. Formerly known as Peninsula Family Service, Fair Oaks Intergenerational Center. Formerly known as the Fair Oaks Senior Center. Formerly known as Family Service Agency of San Mateo County, Fair Oaks Intergenerational Center.","short_desc":"A multipurpose senior citizens' center serving the Redwood City area.","address_attributes":{"street":"2600 Middlefield Road","city":"Redwood City","state":"CA","zip":"94063"},"mail_address_attributes":{"attention":"Fair Oaks Intergenerational Center","street":"2600 Middlefield Road","city":"Redwood City","state":"CA","zip":"94063"},"hours":"Monday-Friday, 9-5","transportation":"SAMTRANS stops in front.","accessibility":["ramp","restroom","disabled_parking","wheelchair"],"languages":["Filipino (Tagalog)","Spanish"],"emails":["cgonzalez@peninsulafamilyservice.org"],"faxes_attributes":[{"number":"650 701-0856"}],"phones_attributes":[{"number":"650 780-7525"}],"urls":["http://www.peninsulafamilyservice.org"],"services_attributes":[{"audience":"Older adults age 55 or over, ethnic minorities and low-income persons","eligibility":"Age 55 or over for most programs, age 60 or over for lunch program","fees":"$2.50 suggested donation for lunch for age 60 or over, donations for other services appreciated. Cash and checks accepted.","how_to_apply":"Walk in or apply by phone.","service_areas":["Redwood City"],"keywords":["ADULT PROTECTION AND CARE SERVICES","Meal Sites/Home-delivered Mea","COMMUNITY SERVICES","Group Support","Information and Referral","EDUCATION SERVICES","English Language","RECREATION/LEISURE SERVICES","Arts and Crafts","Sports/Games/Exercise","Brown Bag Food Programs","Congregate Meals/Nutrition Sites","Senior Centers","Older Adults"],"wait":"No wait.","funding_sources":["City","County","Donations","Fees","Fundraising"]}]},{"name":"Second Career Employment Program","contacts_attributes":[{"name":"Brenda Brown","title":"Director, Second Career Services"}],"description":"Provides training and job placement to eligible people age 55 or over who meet certain income qualifications. An income of 125% of poverty level or less is required for subsidized employment and training. (No income requirements for job matchup program.) If a person seems likely to be qualified after a preliminary phone call or visit, he or she must complete an application at this office. Staff will locate appropriate placements for applicants, provide orientation and on-the-job training and assist with finding a job outside the program. Any county resident, regardless of income, age 55 or over has access to the program, job developers and the job bank. Also provides referrals to classroom training. Formerly known as Family Service Agency of San Mateo County, Senior Employment Services.","short_desc":"Provides training and job placement to eligible persons age 55 or over. All persons age 55 or over have access to information in the program's job bank.","address_attributes":{"street":"24 Second Avenue","city":"San Mateo","state":"CA","zip":"94401"},"mail_address_attributes":{"attention":"PFS Second Career Services","street":"24 Second Avenue","city":"San Mateo","state":"CA","zip":"94401"},"hours":"Monday-Friday, 9-5","transportation":"SAMTRANS stops within 1 block, CALTRAIN stops within 6 blocks.","accessibility":["wheelchair"],"languages":["Chinese (Mandarin)","Filipino (Tagalog)","Italian","Spanish"],"emails":["bbrown@peninsulafamilyservice.org"],"faxes_attributes":[{"number":"650 403-4302"}],"phones_attributes":[{"number":"650 403-4300","extension":" Ext. 4385"}],"urls":["http://www.peninsulafamilyservice.org"],"services_attributes":[{"audience":"Residents of San Mateo County age 55 or over","eligibility":"Age 55 or over, county resident and willing and able to work. Income requirements vary according to program","fees":"None. Donations requested of clients who can afford it.","how_to_apply":"Apply by phone for an appointment.","service_areas":["San Mateo County"],"keywords":["EMPLOYMENT/TRAINING SERVICES","Job Development","Job Information/Placement/Referral","Job Training","Job Training Formats","Job Search/Placement","Older Adults"],"wait":"Varies.","funding_sources":["County","Federal","State"]}]},{"name":"Senior Peer Counseling","contacts_attributes":[{"name":"Howard Lader, LCSW","title":"Manager, Senior Peer Counseling"}],"description":"Offers supportive counseling services to San Mateo County residents age 55 or over. Volunteer counselors are selected and professionally trained to help their peers face the challenges and concerns of growing older. Training provides topics that include understanding depression, cultural competence, sexuality, community resources, and other subjects that are relevant to working with older adults. After completion of training, weekly supervision groups are provided for the volunteer senior peer counselors, as well as quarterly in-service training seminars. Peer counseling services are provided in English, Spanish, Chinese (Cantonese and Mandarin), Tagalog, and to the lesbian, gay, bisexual, transgender older adult community. Formerly known as Family Service Agency of San Mateo County, Senior Peer Counseling.","short_desc":"Offers supportive counseling services to older persons.","address_attributes":{"street":"24 Second Avenue","city":"San Mateo","state":"CA","zip":"94403"},"mail_address_attributes":{"attention":"PFS Senior Peer Counseling","street":"24 Second Avenue","city":"San Mateo","state":"CA","zip":"94403"},"hours":"Any time that is convenient for the client and peer counselor","transportation":"Service is provided in person's home or at a mutually agreed upon location.","accessibility":[],"languages":["Chinese (Cantonese)","Chinese (Mandarin)","Filipino (Tagalog)","Spanish"],"emails":null,"faxes_attributes":[{"number":"650 403-4303"}],"phones_attributes":[{"number":"650 403-4300","department":"English"}],"urls":null,"services_attributes":[{"audience":"Older adults age 55 or over who can benefit from counseling","eligibility":"Resident of San Mateo County age 55 or over","fees":"None.","how_to_apply":"Phone for information (403-4300 Ext. 4322).","service_areas":["San Mateo County"],"keywords":["Geriatric Counseling","Older Adults","Gay, Lesbian, Bisexual, Transgender Individuals"],"wait":"Varies.","funding_sources":["County","Donations","Grants"]}]},{"name":"Family Visitation Center","contacts_attributes":[{"name":"Kimberly Pesavento","title":"Director of Visitation"}],"description":"Provides supervised visitation services and a neutral site for parents to carry out supervised exchanges of children in a safe manner. Therapeutic visitation and other counseling services available. Kids in the Middle is an education class for separating or divorcing parents. The goal is to enable parents to focus on their children's needs while the family is going through separation, divorce or other transition. The class explores the psychological aspects of divorce and its impact on children, builds effective communication techniques and points out areas in which outside help may be indicated. The fee is $50 for working parents, $15 for unemployed people. Classes are scheduled on Saturdays and Sundays and held at various sites throughout the county. Call 650-403-4300 ext. 4500 to register. Formerly known as Family Service Agency of San Mateo County, Family Visitation Center.","short_desc":"Provides supervised visitation services and a neutral site for parents in extremely hostile divorces to carry out supervised exchanges of children.","address_attributes":{"street":"24 Second Avenue","city":"San Mateo","state":"CA","zip":"94401"},"mail_address_attributes":{"attention":"PFS Family Visitation Center","street":"24 Second Avenue","city":"San Mateo","state":"CA","zip":"94401"},"hours":"Monday, 10-6; Tuesday-Friday, 10-8; Saturday, Sunday, 9:30-5:30","transportation":"SAMTRANS stops within 1 block, CALTRAIN stops within 4 blocks.","accessibility":["wheelchair"],"languages":["Spanish"],"emails":["kpesavento@peninsulafamilyservice.org"],"faxes_attributes":[{"number":"650 403-4303"}],"phones_attributes":[{"number":"650 403-4300","extension":"4500"}],"urls":["http://www.peninsulafamilyservice.org"],"services_attributes":[{"audience":"Parents, children, families with problems of custody disputes, domestic violence or substance abuse, families going through a separation or divorce","eligibility":"None","fees":"Vary according to income ($5-$90). Cash, checks and credit cards accepted.","how_to_apply":"Apply by phone.","service_areas":["San Mateo County"],"keywords":["INDIVIDUAL AND FAMILY DEVELOPMENT SERVICES","Growth and Adjustment","LEGAL AND CRIMINAL JUSTICE SERVICES","Mediation","Parental Visitation Monitoring","Divorce Counseling","Youth"],"wait":"No wait.","funding_sources":["County","Donations","Grants"]}]},{"name":"Economic Self - Sufficiency Program","contacts_attributes":[{"name":"Joe Bloom","title":"Financial Empowerment Programs Program Director"}],"description":"Provides fixed 8% short term loans to eligible applicants for the purchase of a reliable, used autmobile. Loans are up to $6,000 over 2 1/2 years (30 months). Funds must go towards the entire purchase of the automobile. Peninsula Family Service originates loans and collaborates with commercial partner banks to service the loans, helping clients build credit histories. Formerly known as Family Service Agency of San Mateo County, Ways to Work Family Loan Program.","short_desc":"Makes small loans to working families.","address_attributes":{"street":"24 Second Avenue","city":"San Mateo","state":"CA","zip":"94401"},"mail_address_attributes":{"attention":"Economic Self - Sufficiency Program","street":"24 Second Avenue","city":"San Mateo","state":"CA","zip":"94401"},"hours":null,"transportation":"SAMTRANS stops within 1 block. CALTRAIN stops within 6 blocks.","accessibility":["wheelchair"],"languages":["Hindi","Spanish"],"emails":["waystowork@peninsulafamilyservice.org"],"faxes_attributes":[{"number":"650 403-4303"}],"phones_attributes":[{"number":"650 403-4300","extension":"4100"}],"urls":["http://www.peninsulafamilyservice.org"],"services_attributes":[{"audience":"Target group: Low-income working families with children transitioning from welfare to work and poor or who do not have access to conventional credit","eligibility":"Eligibility: Low-income family with legal custody of a minor child or an involved parent of a dependent minor child. Must reside and/or work in San Mateo County. Must be working and have verifiable income and ability to pay off loan. No bankruptcies in the past two years and unable to qualify for other funding sources. Loans approved by loan committee.","fees":"$60 application fee. Cash or checks accepted.","how_to_apply":"Phone for information.","service_areas":["San Mateo County"],"keywords":["COMMUNITY SERVICES","Speakers","Automobile Loans"],"wait":null,"funding_sources":["County","Grants","State"]}]}]} -{"name":"Peninsula Volunteers","locations":[{"name":"Little House","contacts_attributes":[{"name":"Peter Olson","title":"Little House Director"},{"name":" Bart Charlow","title":"Executive Director, Peninsula Volunteers"}],"description":"A multipurpose center offering a wide variety of recreational, education and cultural activities. Lifelong learning courses cover topics such as music, art, languages, etc., are hosted at this location. Little House offers a large variety of classes including arts and crafts, jewelry, languages, current events, lapidary, woodwork, painting, and fitness courses (yoga, strength straining, tai chi). There are monthly art and cultural lectures, movie showings, and a computer center. Recreation activities include mah jong, pinochle, ballroom dancing, bridge, trips and tours. Partners with the Sequoia Adult Education Program. The Alzheimer's Cafe, open the third Tuesday of every month from 2:00 - 4:00 pm, is a place that brings together people liviing with dementia, their families, and their caregivers. Free and no registration is needed. The Little House Community Service Desk offers information and referrals regarding social service issues, such as housing, food, transportation, health insurance counseling, and estate planning. Massage, podiatry, and acupuncture are available by appointment. Lunch is served Monday-Friday, 11:30 am-1:00 pm. Prices vary according to selection.","short_desc":"A multipurpose senior citizens' center.","address_attributes":{"street":"800 Middle Avenue","city":"Menlo Park","state":"CA","zip":"94025-9881"},"mail_address_attributes":{"attention":"Little House","street":"800 Middle Avenue","city":"Menlo Park","state":"CA","zip":"94025-9881"},"hours":"Monday-Thursday, 8 am-9 pm; Friday, 8-5","transportation":"SAMTRANS stops within 3 blocks, RediWheels and Menlo Park Shuttle stop at door.","accessibility":["disabled_parking","wheelchair"],"languages":["Filipino (Tagalog)","Spanish"],"emails":["polson@peninsulavolunteers.org"],"faxes_attributes":[{"number":"650 326-9547"}],"phones_attributes":[{"number":"650 326-2025"}],"urls":["http://www.penvol.org/littlehouse"],"services_attributes":[{"audience":"Any age","eligibility":"None","fees":"$55 per year membership dues. Classes have fees. Discounts are available for members. Cash, checks and credit cards accepted.","how_to_apply":"Walk in or apply by phone for membership application.","service_areas":["San Mateo County","Santa Clara County"],"keywords":["ADULT PROTECTION AND CARE SERVICES","In-Home Supportive","Meal Sites/Home-delivered Meals","COMMUNITY SERVICES","Group Support","Information and Referral","EDUCATION SERVICES","Adult","HEALTH SERVICES","Education/Information","Family Support","Individual/Group Counseling","Screening/Immunization","RECREATION/LEISURE SERVICES","Sports/Games/Exercise","Community Adult Schools","Senior Centers","Older Adults"],"wait":"No wait.","funding_sources":["Fees","Fundraising","Grants","Membership dues"]}]},{"name":"Rosener House Adult Day Services","contacts_attributes":[{"name":"Bart Charlow","title":"Executive Director, Peninsula Volunteers"},{"name":" Barbara Kalt","title":"Director"}],"description":"Rosener House is a day center for older adults who may be unable to live independently but do not require 24-hour nursing care, may be isolated and in need of a planned activity program, may need assistance with activities of daily living or are living in a family situation where the caregiver needs respite from giving full-time care. Assists elderly persons to continue to live with family or alone rather than moving to a skilled nursing facility. Activities are scheduled Monday-Friday, 10 am-2:30 pm, and participants may come two to five days per week. The facility is open from 8 am to 5:30 pm for participants who need to remain all day. Small group late afternoon activities are held from 3-5:30 pm. The program provides a noon meal including special diets as required. Services offered include social and therapeutic recreational activities, individual and family counseling and occupational, physical and speech therapy. A registered nurse is available daily. The Dementia and Alzheimer's Services Program provides specialized activities in a supportive environment for participants with Alzheimer's disease and other dementias. Holds a weekly support group for caregivers. An early memory loss class for independent adults, \"Minds in Motion\" meets weekly at Rosener House on Wednesday mornings. Call for more information.","short_desc":"A day center for adults age 50 or over.","address_attributes":{"street":"500 Arbor Road","city":"Menlo Park","state":"CA","zip":"94025"},"mail_address_attributes":{"attention":"Rosener House","street":"500 Arbor Road","city":"Menlo Park","state":"CA","zip":"94025"},"hours":"Monday-Friday, 8-5:30","transportation":"Transportation can be arranged via Redi-Wheels or Outreach.","accessibility":["ramp","restroom","disabled_parking","wheelchair"],"languages":["Spanish","Filipino (Tagalog)","Vietnamese"],"emails":["bkalt@peninsulavolunteers.org","fmarchick@peninsulavolunteers.org"],"faxes_attributes":[{"number":"650 322-4067"}],"phones_attributes":[{"number":"650 322-0126"}],"urls":["http://www.penvol.org/rosenerhouse"],"services_attributes":[{"audience":"Older adults who have memory or sensory loss, mobility limitations and may be lonely and in need of socialization","eligibility":"Age 18 or over","fees":"$85 per day. Vary according to income for those unable to pay full fee. Cash, checks, credit cards, private insurance and vouchers accepted.","how_to_apply":"Apply by phone or be referred by a doctor, social worker or other professional. All prospective participants are interviewed individually before starting the program. A recent physical examination is required, including a TB test.","service_areas":["Atherton","Belmont","Burlingame","East Palo Alto","Los Altos","Los Altos Hills","Menlo Park","Mountain View","Palo Alto","Portola Valley","Redwood City","San Carlos","San Mateo","Sunnyvale","Woodside"],"keywords":["ADULT PROTECTION AND CARE SERVICES","Adult Day Health Care","Dementia Management","Adult Day Programs","Older Adults"],"wait":"No wait.","funding_sources":["Donations","Fees","Grants"]}]},{"name":"Meals on Wheels - South County","contacts_attributes":[{"name":"Marilyn Baker-Venturini","title":"Director"},{"name":" Graciela Hernandez","title":"Assistant Manager"},{"name":" Julie Avelino","title":"Assessment Specialist"}],"description":"Delivers a hot meal to the home of persons age 60 or over who are primarily homebound and unable to prepare their own meals, and have no one to prepare meals. Also, delivers a hot meal to the home of disabled individuals ages 18-59. Meals are delivered between 9 am-1:30 pm, Monday-Friday. Special diets are accommodated: low fat, low sodium, and low sugar.","short_desc":"Will deliver a hot meal to the home of persons age 60 or over who are homebound and unable to prepare their own meals. Can provide special diets.","address_attributes":{"street":"800 Middle Avenue","city":"Menlo Park","state":"CA","zip":"94025-9881"},"mail_address_attributes":{"attention":"Meals on Wheels - South County","street":"800 Middle Avenue","city":"Menlo Park","state":"CA","zip":"94025-9881"},"hours":"Delivery times: Monday-Friday, 9-1:30","transportation":"Not necessary for service.","accessibility":null,"languages":["Spanish"],"emails":["mbaker-venturini@peninsulavolunteers.org"],"faxes_attributes":[{"number":"650 326-9547"}],"phones_attributes":[{"number":"650 323-2022"}],"urls":["http://www.peninsulavolunteers.org"],"services_attributes":[{"audience":"Senior citizens age 60 or over, disabled individuals age 18-59","eligibility":"Homebound person unable to cook or shop","fees":"Suggested donation of $4.25 per meal for seniors 60 or over. Mandatory charge of $2 per meal for disabled individuals ages 18-59.","how_to_apply":"Apply by phone.","service_areas":["Atherton","Belmont","East Palo Alto","Menlo Park","Portola Valley","Redwood City","San Carlos","Woodside"],"keywords":["ADULT PROTECTION AND CARE SERVICES","Meal Sites/Home-delivered Mea","HEALTH SERVICES","Nutrition","Home Delivered Meals","Older Adults","Disabilities Issues"],"wait":"No wait.","funding_sources":["County","Donations"]}]}]} -{"name":"Redwood City Public Library","locations":[{"name":"Fair Oaks Branch","contacts_attributes":[{"name":"Dave Genesy","title":"Library Director"},{"name":" Maria Kramer","title":"Library Divisions Manager"}],"description":"Provides general reading material, including bilingual, multi-cultural books, CDs and cassettes, bilingual and Spanish language reference services. School, class and other group visits may be arranged by appointment. The library is a member of the Peninsula Library System.","short_desc":"Provides general reading materials and reference services.","address_attributes":{"street":"2510 Middlefield Road","city":"Redwood City","state":"CA","zip":"94063"},"mail_address_attributes":{"attention":"Fair Oaks Branch","street":"2510 Middlefield Road","city":"Redwood City","state":"CA","zip":"94063"},"hours":"Monday-Thursday, 10-7; Friday, 10-5","transportation":"SAMTRANS stops in front.","accessibility":["ramp","restroom","wheelchair"],"languages":["Spanish"],"emails":null,"faxes_attributes":[{"number":"650 569-3371"}],"phones_attributes":[{"number":"650 780-7261"}],"urls":null,"services_attributes":[{"audience":"Ethnic minorities, especially Spanish speaking","eligibility":"Resident of California to obtain a library card","fees":"None.","how_to_apply":"Walk in. Proof of residency in California required to receive a library card.","service_areas":["San Mateo County"],"keywords":["EDUCATION SERVICES","Library","Libraries","Public Libraries"],"wait":"No wait.","funding_sources":["City","County"]}]},{"name":"Main Library","contacts_attributes":[{"name":"Dave Genesy","title":"Library Director"},{"name":" Maria Kramer","title":"Library Division Manager"}],"description":"Provides general reading and media materials, literacy and homework assistance, and programs for all ages. Provides public computers, wireless connectivity, a children's room, teen center, and a local history collection. The library is a member of the Peninsula Library System. The Fair Oaks Branch (650-780-7261) is located at 2510 Middlefield Road and is open Monday-Thursday, 10-7; Friday, 10-5. The Schaberg Branch (650-780-7010) is located at 2140 Euclid Avenue and is open Tuesday-Thursday, 1-6; Saturday, 10-3. The Redwood Shores Branch (650-780-5740) is located at 399 Marine Parkway and is open Monday-Thursday, 10-8; Saturday, 10-5; Sunday 12-5.","short_desc":"Provides general reference and reading materials to adults, teenagers and children.","address_attributes":{"street":"1044 Middlefield Road","city":"Redwood City","state":"CA","zip":"94063"},"mail_address_attributes":{"attention":"Main Library","street":"1044 Middlefield Road","city":"Redwood City","state":"CA","zip":"94063"},"hours":"Monday-Thursday, 10-9; Friday, Saturday, 10-5; Sunday, 12-5","transportation":"SAMTRANS stops within 1 block; CALTRAIN stops within 1 block.","accessibility":["elevator","tape_braille","ramp","restroom","disabled_parking","wheelchair"],"languages":["Spanish"],"emails":["rclinfo@redwoodcity.org"],"faxes_attributes":[{"number":"650 780-7069"}],"phones_attributes":[{"number":"650 780-7018","department":"Circulation"}],"urls":["http://www.redwoodcity.org/library"],"services_attributes":[{"audience":null,"eligibility":"Resident of California to obtain a card","fees":"None.","how_to_apply":"Walk in. Proof of California residency to receive a library card.","service_areas":["San Mateo County"],"keywords":["EDUCATION SERVICES","Library","Libraries","Public Libraries"],"wait":"No wait.","funding_sources":["City"]}]},{"name":"Schaberg Branch","contacts_attributes":[{"name":"Dave Genesy","title":"Library Director"},{"name":" Elizabeth Meeks","title":"Branch Manager"}],"description":"Provides general reading materials, including large-type books, DVD's and CDs, books on CD and some Spanish language materials to children. Offers children's programs and a Summer Reading Club. Participates in the Peninsula Library System.","short_desc":"Provides general reading materials and reference services.","address_attributes":{"street":"2140 Euclid Avenue.","city":"Redwood City","state":"CA","zip":"94061"},"mail_address_attributes":{"attention":"Schaberg Branch","street":"2140 Euclid Avenue","city":"Redwood City","state":"CA","zip":"94061"},"hours":"Tuesday-Thursday, 1-6, Saturday, 10-3","transportation":"SAMTRANS stops within 1 block.","accessibility":["ramp"],"languages":null,"emails":null,"faxes_attributes":[{"number":"650 365-3738"}],"phones_attributes":[{"number":"650 780-7010"}],"urls":null,"services_attributes":[{"audience":null,"eligibility":"Resident of California to obtain a library card for borrowing materials","fees":"None.","how_to_apply":"Walk in. Proof of California residency required to receive a library card.","service_areas":["San Mateo County"],"keywords":["EDUCATION SERVICES","Library","Libraries","Public Libraries"],"wait":"No wait.","funding_sources":["City"]}]},{"name":"Project Read","contacts_attributes":[{"name":"Kathy Endaya","title":"Director"}],"description":"Offers an intergenerational literacy program for youth and English-speaking adults seeking to improver literacy skills. Adult services include: adult one-to-one tutoring to improve basic skills in reading, writing and critical thinking; Families for Literacy (FFL), a home-based family literacy program for parents who want to be able to read to their young children; and small group/English as a Second Language (ESL). Youth services include: Youth Tutoring, Families in Partnership (FIP); Teen-to-Elementary Student Tutoring, Kids in Partnership (KIP); and computer-aided literacy. Redwood City Friends of Literacy is a fundraising board that helps to support and to fund Redwood City's Project Read. Call for more information about each service.","short_desc":"Offers an intergenerational literacy program for adults and youth seeking to improver literacy skills.","address_attributes":{"street":"1044 Middlefield Road, 2nd Floor","city":"Redwood City","state":"CA","zip":"94063"},"mail_address_attributes":{"attention":"Project Read","street":"1044 Middlefield Road, 2nd Floor","city":"Redwood City","state":"CA","zip":"94063"},"hours":"Monday-Thursday, 10-8:30; Friday, 10-5","transportation":"SAMTRANS stops within 1 block.","accessibility":["elevator","ramp","restroom","disabled_parking"],"languages":null,"emails":["rclread@redwoodcity.org"],"faxes_attributes":[{"number":"650 780-7004"}],"phones_attributes":[{"number":"650 780-7077"}],"urls":["http://www.projectreadredwoodcity.org"],"services_attributes":[{"audience":"Adults, parents, children in 1st-12th grades in the Redwood City school districta","eligibility":"English-speaking adult reading at or below 7th grade level or child in 1st-12th grade in the Redwood City school districts","fees":"None.","how_to_apply":"Walk in or apply by phone, email or webpage registration.","service_areas":["Redwood City"],"keywords":["EDUCATION SERVICES","Adult","Alternative","Literacy","Literacy Programs","Libraries","Public Libraries","Youth"],"wait":"Depends on availability of tutors for small groups and one-on-one.","funding_sources":["City","Donations","Federal","Grants","State"]}]},{"name":"Redwood Shores Branch","contacts_attributes":[{"name":"Dave Genesy","title":"Library Director"}],"description":"Provides general reading materials, including large-type books, videos, music cassettes and CDs, and books on tape. Offers children's programs and a Summer Reading Club. Meeting room is available to nonprofit groups. Participates in the Peninsula Library System.","short_desc":"Provides general reading materials and reference services.","address_attributes":{"street":"399 Marine Parkway.","city":"Redwood City","state":"CA","zip":"94065"},"mail_address_attributes":{"attention":"Redwood Shores Branch","street":"399 Marine Parkway","city":"Redwood City","state":"CA","zip":"94065"},"hours":null,"transportation":null,"accessibility":null,"languages":null,"emails":null,"phones_attributes":[{"number":"650 780-5740"}],"urls":["http://www.redwoodcity.org/library"],"services_attributes":[{"audience":null,"eligibility":"Resident of California to obtain a library card","fees":"None.","how_to_apply":"Walk in. Proof of California residency required to receive a library card.","service_areas":["San Mateo County"],"keywords":["EDUCATION SERVICES","Library","Libraries","Public Libraries"],"wait":"No wait.","funding_sources":["City"]}]}]} -{"name":"Salvation Army","locations":[{"name":"Redwood City Corps","contacts_attributes":[{"name":"Andres Espinoza","title":"Captain, Commanding Officer"}],"description":"Provides food, clothing, bus tokens and shelter to individuals and families in times of crisis from the Redwood City Corps office and community centers throughout the county. Administers Project REACH (Relief for Energy Assistance through Community Help) funds to prevent energy shut-off through a one-time payment. Counseling and translation services (English/Spanish) are available either on a walk-in basis or by appointment. Rental assistance with available funds. Another office (described separately) is located at 409 South Spruce Avenue, South San Francisco (650-266-4591).","short_desc":"Provides a variety of emergency services to low-income persons. Also sponsors recreational and educational activities.","address_attributes":{"street":"660 Veterans Blvd.","city":"Redwood City","state":"CA","zip":"94063"},"mail_address_attributes":{"attention":"Salvation Army","street":"P.O. Box 1147","city":"Redwood City","state":"CA","zip":"94064"},"hours":null,"transportation":"SAMTRANS stops nearby.","accessibility":["wheelchair"],"languages":["Spanish"],"emails":null,"faxes_attributes":[{"number":"650 364-1712"}],"phones_attributes":[{"number":"650 368-4643"}],"urls":["http://www.tsagoldenstate.org"],"services_attributes":[{"audience":"Individuals or families with low or no income and/or trying to obtain public assistance","eligibility":"None for most services. For emergency assistance, must have low or no income and be willing to apply for public assistance","fees":"None.","how_to_apply":"Call for appointment. Referral from human service professional preferred for emergency assistance.","service_areas":["Atherton","Belmont","Burlingame","East Palo Alto","Foster City","Menlo Park","Palo Alto","Portola Valley","Redwood City","San Carlos","San Mateo","Woodside"],"keywords":["COMMUNITY SERVICES","Interpretation/Translation","EMERGENCY SERVICES","Shelter/Refuge","FINANCIAL ASSISTANCE SERVICES","Utilities","MENTAL HEALTH SERVICES","Individual/Group Counseling","Food Pantries","Homeless Shelter","Rental Deposit Assistance","Utility Service Payment Assistance"],"wait":"Up to 20 minutes.","funding_sources":["Donations","Grants"]}]},{"name":"Adult Rehabilitation Center","contacts_attributes":[{"name":"Jack Phillips","title":"Administrator"}],"description":"Provides a long-term (6-12 month) residential rehabilitation program for men and women with substance abuse and other problems. Residents receive individual counseling, and drug and alcohol education. The spiritual side of recovery is address_attributesed through chapel services and Bible study as well as 12-step programs. Nicotine cessation is a part of the program. Residents must be physically able to work, seeking treatment for substance abuse, sober long enough to pass urine drug screen before entering and agreeable to participating in weekly 12-step programs such as Alcoholics Anonymous or Narcotics Anonymous. Pinehurst Lodge is a separate facility for women only. Transition houses for men and women graduates also available.","short_desc":"Long-term (6-12 month) residential treatment program for men/women age 21-60.","address_attributes":{"street":"1500 Valencia Street","city":"San Francisco","state":"CA","zip":"94110"},"mail_address_attributes":{"attention":"Adult Rehabilitation Center","street":"1500 Valencia Street","city":"San Francisco","state":"CA","zip":"94110"},"hours":"Monday-Friday, 8-4","transportation":"MUNI - 26 Valencia, Mission Street lines.","accessibility":["wheelchair"],"languages":["Spanish"],"emails":null,"faxes_attributes":[{"number":"415 285-1391"}],"phones_attributes":[{"number":"415 643-8000"}],"urls":null,"services_attributes":[{"audience":"Adult alcoholic/drug addictive men and women with social and spiritual problems","eligibility":"Age 21-60, detoxed, physically able and willing to participate in a work therapy program","fees":"None.","how_to_apply":"Walk in or through other agency referral.","service_areas":["Alameda County","Contra Costa County","Marin County","San Francisco County","San Mateo County","Santa Clara County","Northern California"],"keywords":["ALCOHOLISM SERVICES","Residential Care","DRUG ABUSE SERVICES"],"wait":"Varies according to available beds for men and women. Women have a longer wait due to small number of beds statewide.","funding_sources":["Donations","Sales"]}]},{"name":"Sunnyvale Corps","contacts_attributes":[{"name":"James Lee","title":"Commanding Officer"}],"description":"Provides emergency assistance including food and clothing for persons in immediate need. Provides PG&E assistance through REACH program. Youth programs offer tutoring, music and troops. Information on related resources is available. Also provides rental assistance when funds are available.","short_desc":"Provides emergency assistance to persons in immediate need and offers after school activities and summer day camp program.","address_attributes":{"street":"1161 South Bernardo","city":"Sunnyvale","state":"CA","zip":"94087"},"mail_address_attributes":{"attention":"Salvation Army","street":"P.O. Box 61868","city":"Sunnyvale","state":"CA","zip":"94088"},"hours":"Monday-Friday, 9-4","transportation":"VTA stops within 4 blocks.","accessibility":[],"languages":["Korean"],"emails":["william_nichols@usw.salvationarmy.org"],"faxes_attributes":[{"number":"408 720-8075"}],"phones_attributes":[{"number":"408 720-0420"}],"urls":null,"services_attributes":[{"audience":null,"eligibility":"None for emergency assistance","fees":"None for emergency services. Vary for after school activities. Cash and checks accepted.","how_to_apply":"Walk in. Written application, identification required for emergency assistance.","service_areas":["Los Altos","Mountain View","Sunnyvale"],"keywords":["COMMODITY SERVICES","Clothing/Personal Items","CHILD PROTECTION AND CARE SERVICES","Day Care","COMMUNITY SERVICES","Information and Referral","EMERGENCY SERVICES","Food Boxes/Food Vouchers","FINANCIAL ASSISTANCE SERVICES","Utilities","RECREATION/LEISURE SERVICES","Camping","Emergency Food","Clothing","Utility Assistance","Youth Development"],"wait":"No wait.","funding_sources":["Donations","Fees","Grants"]}]},{"name":"South San Francisco Citadel Corps","contacts_attributes":[{"name":"Kenneth Gibson","title":"Major"}],"description":"Provides emergency food, clothing and furniture vouchers to low-income families in times of crisis. Administers Project REACH (Relief for Energy Assistance through Community Help) funds to prevent energy shut-off through a one-time payment. Offers a Saturday morning Homeless Feeding Program at 10:30, as well as Sunday services and spiritual counseling. Provides Christmas toys and Back-to-School clothes and supplies. Offers case management, advocacy and referrals to other agencies.","short_desc":"Provides emergency food and clothing and furniture vouchers to low-income families in times of crisis.","address_attributes":{"street":"409 South Spruce Avenue","city":"South San Francisco","state":"CA","zip":"94080"},"mail_address_attributes":{"attention":"Salvation Army","street":"409 South Spruce Avenue","city":"South San Francisco","state":"CA","zip":"94080"},"hours":"Monday-Thursday, 9-4:30","transportation":"SAMTRANS stops within 1 block, BART stops within 3 blocks.","accessibility":["wheelchair"],"languages":null,"emails":null,"faxes_attributes":[{"number":"650 266-2594"},{"number":"650 266-4594"}],"phones_attributes":[{"number":"650 266-4591"}],"urls":["http://www.tsagoldenstate.org"],"services_attributes":[{"audience":null,"eligibility":"Low-income families","fees":"None.","how_to_apply":"Call for information.","service_areas":["Brisbane","Colma","Daly City","Millbrae","Pacifica","San Bruno","South San Francisco"],"keywords":["COMMODITY SERVICES","Clothing/Personal Items","COMMUNITY SERVICES","Information and Referral","EMERGENCY SERVICES","Food Boxes/Food Vouchers","FINANCIAL ASSISTANCE SERVICES","Utilities","Emergency Food","Food Pantries","Furniture","Clothing","Utility Assistance","School Supplies","Case/Care Management","Holiday Programs","Pastoral Counseling","Low Income"],"wait":null,"funding_sources":["Donations"]}]}]} -{"name":"Samaritan House","locations":[{"name":"Redwood City Free Medical Clinic","contacts_attributes":[{"name":"Sharon Petersen","title":"Administrator"}],"description":"Provides free medical care to those in need. Offers basic medical exams for adults and tuberculosis screening. Assists the individual to access other services in the community. By appointment only, Project Smile provides a free dental exam, dental cleaning and oral hygiene instruction for children, age 3-12, of Samaritan House clients.","short_desc":"Provides free medical care to those in need.","address_attributes":{"street":"114 Fifth Avenue","city":"Redwood City","state":"CA","zip":"94063"},"mail_address_attributes":{"attention":"Redwood City Free Medical Clinic","street":"114 Fifth Avenue","city":"Redwood City","state":"CA","zip":"94063"},"hours":"Monday-Friday, 9-12, 2-5","transportation":"SAMTRANS stops within 2 blocks.","accessibility":["restroom","wheelchair"],"languages":["Spanish"],"emails":["gracie@samaritanhouse.com"],"faxes_attributes":[{"number":"650 839-1457"}],"phones_attributes":[{"number":"650 839-1447"}],"urls":["http://www.samaritanhouse.com"],"services_attributes":[{"audience":null,"eligibility":"Low-income person without access to health care","fees":"None.","how_to_apply":"Call for screening appointment. Medical visits are by appointment only.","service_areas":["Atherton","East Palo Alto","Menlo Park","Redwood City","San Carlos"],"keywords":["HEALTH SERVICES","Outpatient Care","Community Clinics"],"wait":"Varies.","funding_sources":["Donations","Grants"]}]},{"name":"San Mateo Free Medical Clinic","contacts_attributes":[{"name":"Sharon Petersen","title":"Administrator"}],"description":"Provides free medical and dental care to those in need. Offers basic medical care for adults.","short_desc":"Provides free medical and dental care to those in need. Offers basic medical care for adults.","address_attributes":{"street":"19 West 39th Avenue","city":"San Mateo","state":"CA","zip":"94403"},"mail_address_attributes":{"attention":"San Mateo Free Medical/Dental","street":"19 West 39th Avenue","city":"San Mateo","state":"CA","zip":"94403"},"hours":"Monday-Friday, 9-12, 1-4","transportation":"SAMTRANS stops within 1 block.","accessibility":["elevator","ramp","wheelchair"],"languages":["Spanish"],"emails":["smcmed@samaritanhouse.com"],"faxes_attributes":[{"number":"650 578-0440"}],"phones_attributes":[{"number":"650 578-0400"}],"urls":["http://www.samaritanhouse.com"],"services_attributes":[{"audience":null,"eligibility":"Low-income person without access to health care","fees":"None.","how_to_apply":"Call for screening appointment (650-347-3648).","service_areas":["Belmont","Burlingame","Foster City","Millbrae","San Carlos","San Mateo"],"keywords":["HEALTH SERVICES","Outpatient Care","Community Clinics"],"wait":"Varies.","funding_sources":["Donations","Grants"]}]}]} -{"name":"Location with no phone", "locations":[{"accessibility" : [], "description" : "no phone", "emails" : [], "faxes_attributes" : [], "hours" : null, "languages" : null, "mail_address_attributes" : { "attention" : "", "street" : "puma", "city" : "fairfax", "state" : "VA", "zip" : "22031" }, "name" : "Location with no phone", "phones_attributes" : [], "short_desc" : "no phone", "transportation" : null, "urls" : null, "services_attributes":[{"audience":""}] } ] } -{"name":"Admin Test Org", "locations":[{"accessibility" : [ "elevator", "restroom" ], "address_attributes" : { "city" : "fairfax", "state" : "va", "street" : "bozo", "zip" : "12345" }, "contacts_attributes" : [ { "name" : "Moncef", "title" : "Director" } ], "latitude" : 42.8142432, "longitude": -73.9395687, "description" : "This is a description", "emails" : [ "eml@example.org" ], "faxes_attributes" : [ { "number" : "2025551212", "department" : "CalFresh" } ], "hours" : "Monday-Friday 10am-5pm", "languages" : null, "name" : "Admin Test Location", "phones_attributes" : [ { "number" : "7035551212", "vanity_number" : "703555-ABCD", "extension" : "x1223", "department" : "CalFresh" } ], "short_desc" : "This is a short description", "transportation" : "SAMTRANS stops within 1/2 mile.", "urls" : [ "http://codeforamerica.org" ], "services_attributes":[{"service_areas":["San Mateo County"]}] }] } +{"name":"Peninsula Family Service","locations":[{"name":"Fair Oaks Adult Activity Center","contacts_attributes":[{"name":"Susan Houston","title":"Director of Older Adult Services"},{"name":" Christina Gonzalez","title":"Center Director"}],"description":"A walk-in center for older adults that provides social services, wellness, recreational, educational and creative activities including arts and crafts, computer classes and gardening classes. Coffee and healthy breakfasts are available daily. A hot lunch is served Tuesday-Friday for persons age 60 or over and spouse. Provides case management (including in-home assessments) and bilingual information and referral about community services to persons age 60 or over on questions of housing, employment, household help, recreation and social activities, home delivered meals, health and counseling services and services to shut-ins. Health insurance and legal counseling is available by appointment. Lectures on a variety of health and fitness topics are held monthly in both English and Spanish. Provides a variety of physical fitness opportunities, including a garden club, yoga, tai chi, soul line dance and aerobics classes geared toward older adults. Also provides free monthly blood pressure screenings, quarterly blood glucose monitoring and health screenings by a visiting nurse. Offers a Brown Bag Program in which low-income seniors can receive a bag of groceries each week for a membership fee of $10 a year. Offers Spanish lessons. Formerly known as Peninsula Family Service, Fair Oaks Intergenerational Center. Formerly known as the Fair Oaks Senior Center. Formerly known as Family Service Agency of San Mateo County, Fair Oaks Intergenerational Center.","short_desc":"A multipurpose senior citizens' center serving the Redwood City area.","address_attributes":{"street":"2600 Middlefield Road","city":"Redwood City","state":"CA","zip":"94063"},"mail_address_attributes":{"attention":"Fair Oaks Intergenerational Center","street":"2600 Middlefield Road","city":"Redwood City","state":"CA","zip":"94063"},"hours":"Monday-Friday, 9-5","transportation":"SAMTRANS stops in front.","accessibility":["ramp","restroom","disabled_parking","wheelchair"],"languages":["Filipino (Tagalog)","Spanish"],"emails":["cgonzalez@peninsulafamilyservice.org"],"faxes_attributes":[{"number":"650 701-0856"}],"phones_attributes":[{"number":"650 780-7525"}],"urls":["http://www.peninsulafamilyservice.org"],"services_attributes":[{"name":"Fair Oaks Adult Activity Center", "description":"A walk-in center for older adults that provides social services, wellness, recreational, educational and creative activities including arts and crafts, computer classes and gardening classes.","audience":"Older adults age 55 or over, ethnic minorities and low-income persons","eligibility":"Age 55 or over for most programs, age 60 or over for lunch program","fees":"$2.50 suggested donation for lunch for age 60 or over, donations for other services appreciated. Cash and checks accepted.","how_to_apply":"Walk in or apply by phone.","service_areas":["Redwood City"],"keywords":["ADULT PROTECTION AND CARE SERVICES","Meal Sites/Home-delivered Mea","COMMUNITY SERVICES","Group Support","Information and Referral","EDUCATION SERVICES","English Language","RECREATION/LEISURE SERVICES","Arts and Crafts","Sports/Games/Exercise","Brown Bag Food Programs","Congregate Meals/Nutrition Sites","Senior Centers","Older Adults"],"wait":"No wait.","funding_sources":["City","County","Donations","Fees","Fundraising"]}]},{"name":"Second Career Employment Program","contacts_attributes":[{"name":"Brenda Brown","title":"Director, Second Career Services"}],"description":"Provides training and job placement to eligible people age 55 or over who meet certain income qualifications. An income of 125% of poverty level or less is required for subsidized employment and training. (No income requirements for job matchup program.) If a person seems likely to be qualified after a preliminary phone call or visit, he or she must complete an application at this office. Staff will locate appropriate placements for applicants, provide orientation and on-the-job training and assist with finding a job outside the program. Any county resident, regardless of income, age 55 or over has access to the program, job developers and the job bank. Also provides referrals to classroom training. Formerly known as Family Service Agency of San Mateo County, Senior Employment Services.","short_desc":"Provides training and job placement to eligible persons age 55 or over. All persons age 55 or over have access to information in the program's job bank.","address_attributes":{"street":"24 Second Avenue","city":"San Mateo","state":"CA","zip":"94401"},"mail_address_attributes":{"attention":"PFS Second Career Services","street":"24 Second Avenue","city":"San Mateo","state":"CA","zip":"94401"},"hours":"Monday-Friday, 9-5","transportation":"SAMTRANS stops within 1 block, CALTRAIN stops within 6 blocks.","accessibility":["wheelchair"],"languages":["Chinese (Mandarin)","Filipino (Tagalog)","Italian","Spanish"],"emails":["bbrown@peninsulafamilyservice.org"],"faxes_attributes":[{"number":"650 403-4302"}],"phones_attributes":[{"number":"650 403-4300","extension":" Ext. 4385"}],"urls":["http://www.peninsulafamilyservice.org"],"services_attributes":[{"name": "Second Career Employment Program", "description": "Provides training and job placement to eligible people age 55 or over who meet certain income qualifications.", "audience":"Residents of San Mateo County age 55 or over","eligibility":"Age 55 or over, county resident and willing and able to work. Income requirements vary according to program","fees":"None. Donations requested of clients who can afford it.","how_to_apply":"Apply by phone for an appointment.","service_areas":["San Mateo County"],"keywords":["EMPLOYMENT/TRAINING SERVICES","Job Development","Job Information/Placement/Referral","Job Training","Job Training Formats","Job Search/Placement","Older Adults"],"wait":"Varies.","funding_sources":["County","Federal","State"]}]},{"name":"Senior Peer Counseling","contacts_attributes":[{"name":"Howard Lader, LCSW","title":"Manager, Senior Peer Counseling"}],"description":"Offers supportive counseling services to San Mateo County residents age 55 or over. Volunteer counselors are selected and professionally trained to help their peers face the challenges and concerns of growing older. Training provides topics that include understanding depression, cultural competence, sexuality, community resources, and other subjects that are relevant to working with older adults. After completion of training, weekly supervision groups are provided for the volunteer senior peer counselors, as well as quarterly in-service training seminars. Peer counseling services are provided in English, Spanish, Chinese (Cantonese and Mandarin), Tagalog, and to the lesbian, gay, bisexual, transgender older adult community. Formerly known as Family Service Agency of San Mateo County, Senior Peer Counseling.","short_desc":"Offers supportive counseling services to older persons.","address_attributes":{"street":"24 Second Avenue","city":"San Mateo","state":"CA","zip":"94403"},"mail_address_attributes":{"attention":"PFS Senior Peer Counseling","street":"24 Second Avenue","city":"San Mateo","state":"CA","zip":"94403"},"hours":"Any time that is convenient for the client and peer counselor","transportation":"Service is provided in person's home or at a mutually agreed upon location.","accessibility":[],"languages":["Chinese (Cantonese)","Chinese (Mandarin)","Filipino (Tagalog)","Spanish"],"emails":null,"faxes_attributes":[{"number":"650 403-4303"}],"phones_attributes":[{"number":"650 403-4300","department":"English"}],"urls":null,"services_attributes":[{"name": "Senior Peer Counseling", "description": "Offers supportive counseling services to San Mateo County residents age 55 or over.","audience":"Older adults age 55 or over who can benefit from counseling","eligibility":"Resident of San Mateo County age 55 or over","fees":"None.","how_to_apply":"Phone for information (403-4300 Ext. 4322).","service_areas":["San Mateo County"],"keywords":["Geriatric Counseling","Older Adults","Gay, Lesbian, Bisexual, Transgender Individuals"],"wait":"Varies.","funding_sources":["County","Donations","Grants"]}]},{"name":"Family Visitation Center","contacts_attributes":[{"name":"Kimberly Pesavento","title":"Director of Visitation"}],"description":"Provides supervised visitation services and a neutral site for parents to carry out supervised exchanges of children in a safe manner. Therapeutic visitation and other counseling services available. Kids in the Middle is an education class for separating or divorcing parents. The goal is to enable parents to focus on their children's needs while the family is going through separation, divorce or other transition. The class explores the psychological aspects of divorce and its impact on children, builds effective communication techniques and points out areas in which outside help may be indicated. The fee is $50 for working parents, $15 for unemployed people. Classes are scheduled on Saturdays and Sundays and held at various sites throughout the county. Call 650-403-4300 ext. 4500 to register. Formerly known as Family Service Agency of San Mateo County, Family Visitation Center.","short_desc":"Provides supervised visitation services and a neutral site for parents in extremely hostile divorces to carry out supervised exchanges of children.","address_attributes":{"street":"24 Second Avenue","city":"San Mateo","state":"CA","zip":"94401"},"mail_address_attributes":{"attention":"PFS Family Visitation Center","street":"24 Second Avenue","city":"San Mateo","state":"CA","zip":"94401"},"hours":"Monday, 10-6; Tuesday-Friday, 10-8; Saturday, Sunday, 9:30-5:30","transportation":"SAMTRANS stops within 1 block, CALTRAIN stops within 4 blocks.","accessibility":["wheelchair"],"languages":["Spanish"],"emails":["kpesavento@peninsulafamilyservice.org"],"faxes_attributes":[{"number":"650 403-4303"}],"phones_attributes":[{"number":"650 403-4300","extension":"4500"}],"urls":["http://www.peninsulafamilyservice.org"],"services_attributes":[{"name":"Family Visitation Center", "description": "Provides supervised visitation services and a neutral site for parents to carry out supervised exchanges of children in a safe manner.", "audience":"Parents, children, families with problems of custody disputes, domestic violence or substance abuse, families going through a separation or divorce","eligibility":"None","fees":"Vary according to income ($5-$90). Cash, checks and credit cards accepted.","how_to_apply":"Apply by phone.","service_areas":["San Mateo County"],"keywords":["INDIVIDUAL AND FAMILY DEVELOPMENT SERVICES","Growth and Adjustment","LEGAL AND CRIMINAL JUSTICE SERVICES","Mediation","Parental Visitation Monitoring","Divorce Counseling","Youth"],"wait":"No wait.","funding_sources":["County","Donations","Grants"]}]},{"name":"Economic Self - Sufficiency Program","contacts_attributes":[{"name":"Joe Bloom","title":"Financial Empowerment Programs Program Director"}],"description":"Provides fixed 8% short term loans to eligible applicants for the purchase of a reliable, used autmobile. Loans are up to $6,000 over 2 1/2 years (30 months). Funds must go towards the entire purchase of the automobile. Peninsula Family Service originates loans and collaborates with commercial partner banks to service the loans, helping clients build credit histories. Formerly known as Family Service Agency of San Mateo County, Ways to Work Family Loan Program.","short_desc":"Makes small loans to working families.","address_attributes":{"street":"24 Second Avenue","city":"San Mateo","state":"CA","zip":"94401"},"mail_address_attributes":{"attention":"Economic Self - Sufficiency Program","street":"24 Second Avenue","city":"San Mateo","state":"CA","zip":"94401"},"hours":null,"transportation":"SAMTRANS stops within 1 block. CALTRAIN stops within 6 blocks.","accessibility":["wheelchair"],"languages":["Hindi","Spanish"],"emails":["waystowork@peninsulafamilyservice.org"],"faxes_attributes":[{"number":"650 403-4303"}],"phones_attributes":[{"number":"650 403-4300","extension":"4100"}],"urls":["http://www.peninsulafamilyservice.org"],"services_attributes":[{"name": "Economic Self-Sufficiency Program","description": "Provides fixed 8% short term loans to eligible applicants for the purchase of a reliable, used autmobile. Loans are up to $6,000 over 2 1/2 years (30 months). Funds must go towards the entire purchase of the automobile. Peninsula Family Service originates loans and collaborates with commercial partner banks to service the loans, helping clients build credit histories. Formerly known as Family Service Agency of San Mateo County, Ways to Work Family Loan Program.","audience":"Low-income working families with children transitioning from welfare to work and poor or who do not have access to conventional credit","eligibility":"Eligibility: Low-income family with legal custody of a minor child or an involved parent of a dependent minor child. Must reside and/or work in San Mateo County. Must be working and have verifiable income and ability to pay off loan. No bankruptcies in the past two years and unable to qualify for other funding sources. Loans approved by loan committee.","fees":"$60 application fee. Cash or checks accepted.","how_to_apply":"Phone for information.","service_areas":["San Mateo County"],"keywords":["COMMUNITY SERVICES","Speakers","Automobile Loans"],"wait":null,"funding_sources":["County","Grants","State"]}]}]} +{"name":"Peninsula Volunteers","locations":[{"name":"Little House","contacts_attributes":[{"name":"Peter Olson","title":"Little House Director"},{"name":" Bart Charlow","title":"Executive Director, Peninsula Volunteers"}],"description":"A multipurpose center offering a wide variety of recreational, education and cultural activities. Lifelong learning courses cover topics such as music, art, languages, etc., are hosted at this location. Little House offers a large variety of classes including arts and crafts, jewelry, languages, current events, lapidary, woodwork, painting, and fitness courses (yoga, strength straining, tai chi). There are monthly art and cultural lectures, movie showings, and a computer center. Recreation activities include mah jong, pinochle, ballroom dancing, bridge, trips and tours. Partners with the Sequoia Adult Education Program. The Alzheimer's Cafe, open the third Tuesday of every month from 2:00 - 4:00 pm, is a place that brings together people liviing with dementia, their families, and their caregivers. Free and no registration is needed. The Little House Community Service Desk offers information and referrals regarding social service issues, such as housing, food, transportation, health insurance counseling, and estate planning. Massage, podiatry, and acupuncture are available by appointment. Lunch is served Monday-Friday, 11:30 am-1:00 pm. Prices vary according to selection.","short_desc":"A multipurpose senior citizens' center.","address_attributes":{"street":"800 Middle Avenue","city":"Menlo Park","state":"CA","zip":"94025-9881"},"mail_address_attributes":{"attention":"Little House","street":"800 Middle Avenue","city":"Menlo Park","state":"CA","zip":"94025-9881"},"hours":"Monday-Thursday, 8 am-9 pm; Friday, 8-5","transportation":"SAMTRANS stops within 3 blocks, RediWheels and Menlo Park Shuttle stop at door.","accessibility":["disabled_parking","wheelchair"],"languages":["Filipino (Tagalog)","Spanish"],"emails":["polson@peninsulavolunteers.org"],"faxes_attributes":[{"number":"650 326-9547"}],"phones_attributes":[{"number":"650 326-2025"}],"urls":["http://www.penvol.org/littlehouse"],"services_attributes":[{"name":"Little House Recreational Activities","description": "A multipurpose center offering a wide variety of recreational, education and cultural activities.","audience":"Any age","eligibility":"None","fees":"$55 per year membership dues. Classes have fees. Discounts are available for members. Cash, checks and credit cards accepted.","how_to_apply":"Walk in or apply by phone for membership application.","service_areas":["San Mateo County","Santa Clara County"],"keywords":["ADULT PROTECTION AND CARE SERVICES","In-Home Supportive","Meal Sites/Home-delivered Meals","COMMUNITY SERVICES","Group Support","Information and Referral","EDUCATION SERVICES","Adult","HEALTH SERVICES","Education/Information","Family Support","Individual/Group Counseling","Screening/Immunization","RECREATION/LEISURE SERVICES","Sports/Games/Exercise","Community Adult Schools","Senior Centers","Older Adults"],"wait":"No wait.","funding_sources":["Fees","Fundraising","Grants","Membership dues"]}]},{"name":"Rosener House Adult Day Services","contacts_attributes":[{"name":"Bart Charlow","title":"Executive Director, Peninsula Volunteers"},{"name":" Barbara Kalt","title":"Director"}],"description":"Rosener House is a day center for older adults who may be unable to live independently but do not require 24-hour nursing care, may be isolated and in need of a planned activity program, may need assistance with activities of daily living or are living in a family situation where the caregiver needs respite from giving full-time care. Assists elderly persons to continue to live with family or alone rather than moving to a skilled nursing facility. Activities are scheduled Monday-Friday, 10 am-2:30 pm, and participants may come two to five days per week. The facility is open from 8 am to 5:30 pm for participants who need to remain all day. Small group late afternoon activities are held from 3-5:30 pm. The program provides a noon meal including special diets as required. Services offered include social and therapeutic recreational activities, individual and family counseling and occupational, physical and speech therapy. A registered nurse is available daily. The Dementia and Alzheimer's Services Program provides specialized activities in a supportive environment for participants with Alzheimer's disease and other dementias. Holds a weekly support group for caregivers. An early memory loss class for independent adults, \"Minds in Motion\" meets weekly at Rosener House on Wednesday mornings. Call for more information.","short_desc":"A day center for adults age 50 or over.","address_attributes":{"street":"500 Arbor Road","city":"Menlo Park","state":"CA","zip":"94025"},"mail_address_attributes":{"attention":"Rosener House","street":"500 Arbor Road","city":"Menlo Park","state":"CA","zip":"94025"},"hours":"Monday-Friday, 8-5:30","transportation":"Transportation can be arranged via Redi-Wheels or Outreach.","accessibility":["ramp","restroom","disabled_parking","wheelchair"],"languages":["Spanish","Filipino (Tagalog)","Vietnamese"],"emails":["bkalt@peninsulavolunteers.org","fmarchick@peninsulavolunteers.org"],"faxes_attributes":[{"number":"650 322-4067"}],"phones_attributes":[{"number":"650 322-0126"}],"urls":["http://www.penvol.org/rosenerhouse"],"services_attributes":[{"name": "Rosener House Adult Day Services","description": "Rosener House is a day center for older adults who may be unable to live independently but do not require 24-hour nursing care, may be isolated and in need of a planned activity program, may need assistance with activities of daily living or are living in a family situation where the caregiver needs respite from giving full-time care. Assists elderly persons to continue to live with family or alone rather than moving to a skilled nursing facility. Activities are scheduled Monday-Friday, 10 am-2:30 pm, and participants may come two to five days per week. The facility is open from 8 am to 5:30 pm for participants who need to remain all day. Small group late afternoon activities are held from 3-5:30 pm. The program provides a noon meal including special diets as required. Services offered include social and therapeutic recreational activities, individual and family counseling and occupational, physical and speech therapy. A registered nurse is available daily.","audience":"Older adults who have memory or sensory loss, mobility limitations and may be lonely and in need of socialization","eligibility":"Age 18 or over","fees":"$85 per day. Vary according to income for those unable to pay full fee. Cash, checks, credit cards, private insurance and vouchers accepted.","how_to_apply":"Apply by phone or be referred by a doctor, social worker or other professional. All prospective participants are interviewed individually before starting the program. A recent physical examination is required, including a TB test.","service_areas":["Atherton","Belmont","Burlingame","East Palo Alto","Los Altos","Los Altos Hills","Menlo Park","Mountain View","Palo Alto","Portola Valley","Redwood City","San Carlos","San Mateo","Sunnyvale","Woodside"],"keywords":["ADULT PROTECTION AND CARE SERVICES","Adult Day Health Care","Dementia Management","Adult Day Programs","Older Adults"],"wait":"No wait.","funding_sources":["Donations","Fees","Grants"]}]},{"name":"Meals on Wheels - South County","contacts_attributes":[{"name":"Marilyn Baker-Venturini","title":"Director"},{"name":" Graciela Hernandez","title":"Assistant Manager"},{"name":" Julie Avelino","title":"Assessment Specialist"}],"description":"Delivers a hot meal to the home of persons age 60 or over who are primarily homebound and unable to prepare their own meals, and have no one to prepare meals. Also, delivers a hot meal to the home of disabled individuals ages 18-59. Meals are delivered between 9 am-1:30 pm, Monday-Friday. Special diets are accommodated: low fat, low sodium, and low sugar.","short_desc":"Will deliver a hot meal to the home of persons age 60 or over who are homebound and unable to prepare their own meals. Can provide special diets.","address_attributes":{"street":"800 Middle Avenue","city":"Menlo Park","state":"CA","zip":"94025-9881"},"mail_address_attributes":{"attention":"Meals on Wheels - South County","street":"800 Middle Avenue","city":"Menlo Park","state":"CA","zip":"94025-9881"},"hours":"Delivery times: Monday-Friday, 9-1:30","transportation":"Not necessary for service.","accessibility":null,"languages":["Spanish"],"emails":["mbaker-venturini@peninsulavolunteers.org"],"faxes_attributes":[{"number":"650 326-9547"}],"phones_attributes":[{"number":"650 323-2022"}],"urls":["http://www.peninsulavolunteers.org"],"services_attributes":[{"name": "Meals on Wheels - South County","description": "Delivers a hot meal to the home of persons age 60 or over who are primarily homebound and unable to prepare their own meals, and have no one to prepare meals. Also, delivers a hot meal to the home of disabled individuals ages 18-59. Meals are delivered between 9 am-1:30 pm, Monday-Friday. Special diets are accommodated: low fat, low sodium, and low sugar.","audience":"Senior citizens age 60 or over, disabled individuals age 18-59","eligibility":"Homebound person unable to cook or shop","fees":"Suggested donation of $4.25 per meal for seniors 60 or over. Mandatory charge of $2 per meal for disabled individuals ages 18-59.","how_to_apply":"Apply by phone.","service_areas":["Atherton","Belmont","East Palo Alto","Menlo Park","Portola Valley","Redwood City","San Carlos","Woodside"],"keywords":["ADULT PROTECTION AND CARE SERVICES","Meal Sites/Home-delivered Mea","HEALTH SERVICES","Nutrition","Home Delivered Meals","Older Adults","Disabilities Issues"],"wait":"No wait.","funding_sources":["County","Donations"]}]}]} +{"name":"Redwood City Public Library","locations":[{"name":"Fair Oaks Branch","contacts_attributes":[{"name":"Dave Genesy","title":"Library Director"},{"name":" Maria Kramer","title":"Library Divisions Manager"}],"description":"Provides general reading material, including bilingual, multi-cultural books, CDs and cassettes, bilingual and Spanish language reference services. School, class and other group visits may be arranged by appointment. The library is a member of the Peninsula Library System.","short_desc":"Provides general reading materials and reference services.","address_attributes":{"street":"2510 Middlefield Road","city":"Redwood City","state":"CA","zip":"94063"},"mail_address_attributes":{"attention":"Fair Oaks Branch","street":"2510 Middlefield Road","city":"Redwood City","state":"CA","zip":"94063"},"hours":"Monday-Thursday, 10-7; Friday, 10-5","transportation":"SAMTRANS stops in front.","accessibility":["ramp","restroom","wheelchair"],"languages":["Spanish"],"emails":null,"faxes_attributes":[{"number":"650 569-3371"}],"phones_attributes":[{"number":"650 780-7261"}],"urls":null,"services_attributes":[{"name": "Fair Oaks Branch","description": "Provides general reading material, including bilingual, multi-cultural books, CDs and cassettes, bilingual and Spanish language reference services. School, class and other group visits may be arranged by appointment. The library is a member of the Peninsula Library System.","audience":"Ethnic minorities, especially Spanish speaking","eligibility":"Resident of California to obtain a library card","fees":"None.","how_to_apply":"Walk in. Proof of residency in California required to receive a library card.","service_areas":["San Mateo County"],"keywords":["EDUCATION SERVICES","Library","Libraries","Public Libraries"],"wait":"No wait.","funding_sources":["City","County"]}]},{"name":"Main Library","contacts_attributes":[{"name":"Dave Genesy","title":"Library Director"},{"name":" Maria Kramer","title":"Library Division Manager"}],"description":"Provides general reading and media materials, literacy and homework assistance, and programs for all ages. Provides public computers, wireless connectivity, a children's room, teen center, and a local history collection. The library is a member of the Peninsula Library System. The Fair Oaks Branch (650-780-7261) is located at 2510 Middlefield Road and is open Monday-Thursday, 10-7; Friday, 10-5. The Schaberg Branch (650-780-7010) is located at 2140 Euclid Avenue and is open Tuesday-Thursday, 1-6; Saturday, 10-3. The Redwood Shores Branch (650-780-5740) is located at 399 Marine Parkway and is open Monday-Thursday, 10-8; Saturday, 10-5; Sunday 12-5.","short_desc":"Provides general reference and reading materials to adults, teenagers and children.","address_attributes":{"street":"1044 Middlefield Road","city":"Redwood City","state":"CA","zip":"94063"},"mail_address_attributes":{"attention":"Main Library","street":"1044 Middlefield Road","city":"Redwood City","state":"CA","zip":"94063"},"hours":"Monday-Thursday, 10-9; Friday, Saturday, 10-5; Sunday, 12-5","transportation":"SAMTRANS stops within 1 block; CALTRAIN stops within 1 block.","accessibility":["elevator","tape_braille","ramp","restroom","disabled_parking","wheelchair"],"languages":["Spanish"],"emails":["rclinfo@redwoodcity.org"],"faxes_attributes":[{"number":"650 780-7069"}],"phones_attributes":[{"number":"650 780-7018","department":"Circulation"}],"urls":["http://www.redwoodcity.org/library"],"services_attributes":[{"name": "Main Library", "description": "Provides general reading and media materials, literacy and homework assistance, and programs for all ages. Provides public computers, wireless connectivity, a children's room, teen center, and a local history collection.","audience":null,"eligibility":"Resident of California to obtain a card","fees":"None.","how_to_apply":"Walk in. Proof of California residency to receive a library card.","service_areas":["San Mateo County"],"keywords":["EDUCATION SERVICES","Library","Libraries","Public Libraries"],"wait":"No wait.","funding_sources":["City"]}]},{"name":"Schaberg Branch","contacts_attributes":[{"name":"Dave Genesy","title":"Library Director"},{"name":" Elizabeth Meeks","title":"Branch Manager"}],"description":"Provides general reading materials, including large-type books, DVD's and CDs, books on CD and some Spanish language materials to children. Offers children's programs and a Summer Reading Club. Participates in the Peninsula Library System.","short_desc":"Provides general reading materials and reference services.","address_attributes":{"street":"2140 Euclid Avenue.","city":"Redwood City","state":"CA","zip":"94061"},"mail_address_attributes":{"attention":"Schaberg Branch","street":"2140 Euclid Avenue","city":"Redwood City","state":"CA","zip":"94061"},"hours":"Tuesday-Thursday, 1-6, Saturday, 10-3","transportation":"SAMTRANS stops within 1 block.","accessibility":["ramp"],"languages":null,"emails":null,"faxes_attributes":[{"number":"650 365-3738"}],"phones_attributes":[{"number":"650 780-7010"}],"urls":null,"services_attributes":[{"name": "Schaberg Branch","description": "Provides general reading materials, including large-type books, DVD's and CDs, books on CD and some Spanish language materials to children. Offers children's programs and a Summer Reading Club. Participates in the Peninsula Library System.","audience":null,"eligibility":"Resident of California to obtain a library card for borrowing materials","fees":"None.","how_to_apply":"Walk in. Proof of California residency required to receive a library card.","service_areas":["San Mateo County"],"keywords":["EDUCATION SERVICES","Library","Libraries","Public Libraries"],"wait":"No wait.","funding_sources":["City"]}]},{"name":"Project Read","contacts_attributes":[{"name":"Kathy Endaya","title":"Director"}],"description":"Offers an intergenerational literacy program for youth and English-speaking adults seeking to improver literacy skills. Adult services include: adult one-to-one tutoring to improve basic skills in reading, writing and critical thinking; Families for Literacy (FFL), a home-based family literacy program for parents who want to be able to read to their young children; and small group/English as a Second Language (ESL). Youth services include: Youth Tutoring, Families in Partnership (FIP); Teen-to-Elementary Student Tutoring, Kids in Partnership (KIP); and computer-aided literacy. Redwood City Friends of Literacy is a fundraising board that helps to support and to fund Redwood City's Project Read. Call for more information about each service.","short_desc":"Offers an intergenerational literacy program for adults and youth seeking to improver literacy skills.","address_attributes":{"street":"1044 Middlefield Road, 2nd Floor","city":"Redwood City","state":"CA","zip":"94063"},"mail_address_attributes":{"attention":"Project Read","street":"1044 Middlefield Road, 2nd Floor","city":"Redwood City","state":"CA","zip":"94063"},"hours":"Monday-Thursday, 10-8:30; Friday, 10-5","transportation":"SAMTRANS stops within 1 block.","accessibility":["elevator","ramp","restroom","disabled_parking"],"languages":null,"emails":["rclread@redwoodcity.org"],"faxes_attributes":[{"number":"650 780-7004"}],"phones_attributes":[{"number":"650 780-7077"}],"urls":["http://www.projectreadredwoodcity.org"],"services_attributes":[{"name": "Project Read","description": "Offers an intergenerational literacy program for youth and English-speaking adults seeking to improver literacy skills. Adult services include: adult one-to-one tutoring to improve basic skills in reading, writing and critical thinking; Families for Literacy (FFL), a home-based family literacy program for parents who want to be able to read to their young children; and small group/English as a Second Language (ESL). Youth services include: Youth Tutoring, Families in Partnership (FIP); Teen-to-Elementary Student Tutoring, Kids in Partnership (KIP); and computer-aided literacy. Redwood City Friends of Literacy is a fundraising board that helps to support and to fund Redwood City's Project Read. Call for more information about each service.","audience":"Adults, parents, children in 1st-12th grades in the Redwood City school districta","eligibility":"English-speaking adult reading at or below 7th grade level or child in 1st-12th grade in the Redwood City school districts","fees":"None.","how_to_apply":"Walk in or apply by phone, email or webpage registration.","service_areas":["Redwood City"],"keywords":["EDUCATION SERVICES","Adult","Alternative","Literacy","Literacy Programs","Libraries","Public Libraries","Youth"],"wait":"Depends on availability of tutors for small groups and one-on-one.","funding_sources":["City","Donations","Federal","Grants","State"]}]},{"name":"Redwood Shores Branch","contacts_attributes":[{"name":"Dave Genesy","title":"Library Director"}],"description":"Provides general reading materials, including large-type books, videos, music cassettes and CDs, and books on tape. Offers children's programs and a Summer Reading Club. Meeting room is available to nonprofit groups. Participates in the Peninsula Library System.","short_desc":"Provides general reading materials and reference services.","address_attributes":{"street":"399 Marine Parkway.","city":"Redwood City","state":"CA","zip":"94065"},"mail_address_attributes":{"attention":"Redwood Shores Branch","street":"399 Marine Parkway","city":"Redwood City","state":"CA","zip":"94065"},"hours":null,"transportation":null,"accessibility":null,"languages":null,"emails":null,"phones_attributes":[{"number":"650 780-5740"}],"urls":["http://www.redwoodcity.org/library"],"services_attributes":[{"name": "Redwood Shores Branch","description": "Provides general reading materials, including large-type books, videos, music cassettes and CDs, and books on tape. Offers children's programs and a Summer Reading Club. Meeting room is available to nonprofit groups. Participates in the Peninsula Library System.","audience":null,"eligibility":"Resident of California to obtain a library card","fees":"None.","how_to_apply":"Walk in. Proof of California residency required to receive a library card.","service_areas":["San Mateo County"],"keywords":["EDUCATION SERVICES","Library","Libraries","Public Libraries"],"wait":"No wait.","funding_sources":["City"]}]}]} +{"name":"Salvation Army","locations":[{"name":"Redwood City Corps","contacts_attributes":[{"name":"Andres Espinoza","title":"Captain, Commanding Officer"}],"description":"Provides food, clothing, bus tokens and shelter to individuals and families in times of crisis from the Redwood City Corps office and community centers throughout the county. Administers Project REACH (Relief for Energy Assistance through Community Help) funds to prevent energy shut-off through a one-time payment. Counseling and translation services (English/Spanish) are available either on a walk-in basis or by appointment. Rental assistance with available funds. Another office (described separately) is located at 409 South Spruce Avenue, South San Francisco (650-266-4591).","short_desc":"Provides a variety of emergency services to low-income persons. Also sponsors recreational and educational activities.","address_attributes":{"street":"660 Veterans Blvd.","city":"Redwood City","state":"CA","zip":"94063"},"mail_address_attributes":{"attention":"Salvation Army","street":"P.O. Box 1147","city":"Redwood City","state":"CA","zip":"94064"},"hours":null,"transportation":"SAMTRANS stops nearby.","accessibility":["wheelchair"],"languages":["Spanish"],"emails":null,"faxes_attributes":[{"number":"650 364-1712"}],"phones_attributes":[{"number":"650 368-4643"}],"urls":["http://www.tsagoldenstate.org"],"services_attributes":[{"name": "Redwood City Corps","description": "Provides food, clothing, bus tokens and shelter to individuals and families in times of crisis from the Redwood City Corps office and community centers throughout the county.","audience":"Individuals or families with low or no income and/or trying to obtain public assistance","eligibility":"None for most services. For emergency assistance, must have low or no income and be willing to apply for public assistance","fees":"None.","how_to_apply":"Call for appointment. Referral from human service professional preferred for emergency assistance.","service_areas":["Atherton","Belmont","Burlingame","East Palo Alto","Foster City","Menlo Park","Palo Alto","Portola Valley","Redwood City","San Carlos","San Mateo","Woodside"],"keywords":["COMMUNITY SERVICES","Interpretation/Translation","EMERGENCY SERVICES","Shelter/Refuge","FINANCIAL ASSISTANCE SERVICES","Utilities","MENTAL HEALTH SERVICES","Individual/Group Counseling","Food Pantries","Homeless Shelter","Rental Deposit Assistance","Utility Service Payment Assistance"],"wait":"Up to 20 minutes.","funding_sources":["Donations","Grants"]}]},{"name":"Adult Rehabilitation Center","contacts_attributes":[{"name":"Jack Phillips","title":"Administrator"}],"description":"Provides a long-term (6-12 month) residential rehabilitation program for men and women with substance abuse and other problems. Residents receive individual counseling, and drug and alcohol education. The spiritual side of recovery is addressed through chapel services and Bible study as well as 12-step programs. Nicotine cessation is a part of the program. Residents must be physically able to work, seeking treatment for substance abuse, sober long enough to pass urine drug screen before entering and agreeable to participating in weekly 12-step programs such as Alcoholics Anonymous or Narcotics Anonymous. Pinehurst Lodge is a separate facility for women only. Transition houses for men and women graduates also available.","short_desc":"Long-term (6-12 month) residential treatment program for men/women age 21-60.","address_attributes":{"street":"1500 Valencia Street","city":"San Francisco","state":"CA","zip":"94110"},"mail_address_attributes":{"attention":"Adult Rehabilitation Center","street":"1500 Valencia Street","city":"San Francisco","state":"CA","zip":"94110"},"hours":"Monday-Friday, 8-4","transportation":"MUNI - 26 Valencia, Mission Street lines.","accessibility":["wheelchair"],"languages":["Spanish"],"emails":null,"faxes_attributes":[{"number":"415 285-1391"}],"phones_attributes":[{"number":"415 643-8000"}],"urls":null,"services_attributes":[{"name": "Adult Rehabilitation Center","description": "Provides a long-term (6-12 month) residential rehabilitation program for men and women with substance abuse and other problems. Residents receive individual counseling, and drug and alcohol education. The spiritual side of recovery is addressed through chapel services and Bible study as well as 12-step programs. Nicotine cessation is a part of the program. Residents must be physically able to work, seeking treatment for substance abuse, sober long enough to pass urine drug screen before entering and agreeable to participating in weekly 12-step programs such as Alcoholics Anonymous or Narcotics Anonymous. Pinehurst Lodge is a separate facility for women only. Transition houses for men and women graduates also available.","audience":"Adult alcoholic/drug addictive men and women with social and spiritual problems","eligibility":"Age 21-60, detoxed, physically able and willing to participate in a work therapy program","fees":"None.","how_to_apply":"Walk in or through other agency referral.","service_areas":["Alameda County","Contra Costa County","Marin County","San Francisco County","San Mateo County","Santa Clara County","Northern California"],"keywords":["ALCOHOLISM SERVICES","Residential Care","DRUG ABUSE SERVICES"],"wait":"Varies according to available beds for men and women. Women have a longer wait due to small number of beds statewide.","funding_sources":["Donations","Sales"]}]},{"name":"Sunnyvale Corps","contacts_attributes":[{"name":"James Lee","title":"Commanding Officer"}],"description":"Provides emergency assistance including food and clothing for persons in immediate need. Provides PG&E assistance through REACH program. Youth programs offer tutoring, music and troops. Information on related resources is available. Also provides rental assistance when funds are available.","short_desc":"Provides emergency assistance to persons in immediate need and offers after school activities and summer day camp program.","address_attributes":{"street":"1161 South Bernardo","city":"Sunnyvale","state":"CA","zip":"94087"},"mail_address_attributes":{"attention":"Salvation Army","street":"P.O. Box 61868","city":"Sunnyvale","state":"CA","zip":"94088"},"hours":"Monday-Friday, 9-4","transportation":"VTA stops within 4 blocks.","accessibility":[],"languages":["Korean"],"emails":["william_nichols@usw.salvationarmy.org"],"faxes_attributes":[{"number":"408 720-8075"}],"phones_attributes":[{"number":"408 720-0420"}],"urls":null,"services_attributes":[{"name": "Sunnyvale Corps","description": "Provides emergency assistance including food and clothing for persons in immediate need. Provides PG&E assistance through REACH program. Youth programs offer tutoring, music and troops. Information on related resources is available. Also provides rental assistance when funds are available.","audience":null,"eligibility":"None for emergency assistance","fees":"None for emergency services. Vary for after school activities. Cash and checks accepted.","how_to_apply":"Walk in. Written application, identification required for emergency assistance.","service_areas":["Los Altos","Mountain View","Sunnyvale"],"keywords":["COMMODITY SERVICES","Clothing/Personal Items","CHILD PROTECTION AND CARE SERVICES","Day Care","COMMUNITY SERVICES","Information and Referral","EMERGENCY SERVICES","Food Boxes/Food Vouchers","FINANCIAL ASSISTANCE SERVICES","Utilities","RECREATION/LEISURE SERVICES","Camping","Emergency Food","Clothing","Utility Assistance","Youth Development"],"wait":"No wait.","funding_sources":["Donations","Fees","Grants"]}]},{"name":"South San Francisco Citadel Corps","contacts_attributes":[{"name":"Kenneth Gibson","title":"Major"}],"description":"Provides emergency food, clothing and furniture vouchers to low-income families in times of crisis. Administers Project REACH (Relief for Energy Assistance through Community Help) funds to prevent energy shut-off through a one-time payment. Offers a Saturday morning Homeless Feeding Program at 10:30, as well as Sunday services and spiritual counseling. Provides Christmas toys and Back-to-School clothes and supplies. Offers case management, advocacy and referrals to other agencies.","short_desc":"Provides emergency food and clothing and furniture vouchers to low-income families in times of crisis.","address_attributes":{"street":"409 South Spruce Avenue","city":"South San Francisco","state":"CA","zip":"94080"},"mail_address_attributes":{"attention":"Salvation Army","street":"409 South Spruce Avenue","city":"South San Francisco","state":"CA","zip":"94080"},"hours":"Monday-Thursday, 9-4:30","transportation":"SAMTRANS stops within 1 block, BART stops within 3 blocks.","accessibility":["wheelchair"],"languages":null,"emails":null,"faxes_attributes":[{"number":"650 266-2594"},{"number":"650 266-4594"}],"phones_attributes":[{"number":"650 266-4591"}],"urls":["http://www.tsagoldenstate.org"],"services_attributes":[{"name": "South San Francisco Citadel Corps","description": "Provides emergency food, clothing and furniture vouchers to low-income families in times of crisis. Administers Project REACH (Relief for Energy Assistance through Community Help) funds to prevent energy shut-off through a one-time payment. Offers a Saturday morning Homeless Feeding Program at 10:30, as well as Sunday services and spiritual counseling. Provides Christmas toys and Back-to-School clothes and supplies. Offers case management, advocacy and referrals to other agencies.","audience":null,"eligibility":"Low-income families","fees":"None.","how_to_apply":"Call for information.","service_areas":["Brisbane","Colma","Daly City","Millbrae","Pacifica","San Bruno","South San Francisco"],"keywords":["COMMODITY SERVICES","Clothing/Personal Items","COMMUNITY SERVICES","Information and Referral","EMERGENCY SERVICES","Food Boxes/Food Vouchers","FINANCIAL ASSISTANCE SERVICES","Utilities","Emergency Food","Food Pantries","Furniture","Clothing","Utility Assistance","School Supplies","Case/Care Management","Holiday Programs","Pastoral Counseling","Low Income"],"wait":null,"funding_sources":["Donations"]}]}]} +{"name":"Samaritan House","locations":[{"name":"Redwood City Free Medical Clinic","contacts_attributes":[{"name":"Sharon Petersen","title":"Administrator"}],"description":"Provides free medical care to those in need. Offers basic medical exams for adults and tuberculosis screening. Assists the individual to access other services in the community. By appointment only, Project Smile provides a free dental exam, dental cleaning and oral hygiene instruction for children, age 3-12, of Samaritan House clients.","short_desc":"Provides free medical care to those in need.","address_attributes":{"street":"114 Fifth Avenue","city":"Redwood City","state":"CA","zip":"94063"},"mail_address_attributes":{"attention":"Redwood City Free Medical Clinic","street":"114 Fifth Avenue","city":"Redwood City","state":"CA","zip":"94063"},"hours":"Monday-Friday, 9-12, 2-5","transportation":"SAMTRANS stops within 2 blocks.","accessibility":["restroom","wheelchair"],"languages":["Spanish"],"emails":["gracie@samaritanhouse.com"],"faxes_attributes":[{"number":"650 839-1457"}],"phones_attributes":[{"number":"650 839-1447"}],"urls":["http://www.samaritanhouse.com"],"services_attributes":[{"name": "Project Smile","description":"By appointment only, Project Smile provides a free dental exam, dental cleaning and oral hygiene instruction for children, age 3-12, of Samaritan House clients.","audience":null,"eligibility":"Low-income person without access to health care","fees":"None.","how_to_apply":"Call for screening appointment. Medical visits are by appointment only.","service_areas":["Atherton","East Palo Alto","Menlo Park","Redwood City","San Carlos"],"keywords":["HEALTH SERVICES","Outpatient Care","Community Clinics"],"wait":"Varies.","funding_sources":["Donations","Grants"]}]},{"name":"San Mateo Free Medical Clinic","contacts_attributes":[{"name":"Sharon Petersen","title":"Administrator"}],"description":"Provides free medical and dental care to those in need. Offers basic medical care for adults.","short_desc":"Provides free medical and dental care to those in need. Offers basic medical care for adults.","address_attributes":{"street":"19 West 39th Avenue","city":"San Mateo","state":"CA","zip":"94403"},"mail_address_attributes":{"attention":"San Mateo Free Medical/Dental","street":"19 West 39th Avenue","city":"San Mateo","state":"CA","zip":"94403"},"hours":"Monday-Friday, 9-12, 1-4","transportation":"SAMTRANS stops within 1 block.","accessibility":["elevator","ramp","wheelchair"],"languages":["Spanish"],"emails":["smcmed@samaritanhouse.com"],"faxes_attributes":[{"number":"650 578-0440"}],"phones_attributes":[{"number":"650 578-0400"}],"urls":["http://www.samaritanhouse.com"],"services_attributes":[{"name": "San Mateo Free Medical Clinic","description": "Provides free medical and dental care to those in need. Offers basic medical care for adults.","audience":null,"eligibility":"Low-income person without access to health care","fees":"None.","how_to_apply":"Call for screening appointment (650-347-3648).","service_areas":["Belmont","Burlingame","Foster City","Millbrae","San Carlos","San Mateo"],"keywords":["HEALTH SERVICES","Outpatient Care","Community Clinics"],"wait":"Varies.","funding_sources":["Donations","Grants"]}]}]} +{"name":"Location with no phone", "locations":[{"accessibility" : [], "description" : "no phone", "emails" : [], "faxes_attributes" : [], "hours" : null, "languages" : null, "mail_address_attributes" : { "attention" : "", "street" : "puma", "city" : "fairfax", "state" : "VA", "zip" : "22031" }, "name" : "Location with no phone", "phones_attributes" : [], "short_desc" : "no phone", "transportation" : null, "urls" : null, "services_attributes":[{"name":"Service with blank fields","description":"no unrequired fields for this service","audience":""}] } ] } +{"name":"Admin Test Org", "locations":[{"accessibility" : [ "elevator", "restroom" ], "address_attributes" : { "city" : "fairfax", "state" : "va", "street" : "bozo", "zip" : "12345" }, "contacts_attributes" : [ { "name" : "Moncef", "title" : "Director" } ], "latitude" : 42.8142432, "longitude": -73.9395687, "description" : "This is a description", "emails" : [ "eml@example.org" ], "faxes_attributes" : [ { "number" : "2025551212", "department" : "CalFresh" } ], "hours" : "Monday-Friday 10am-5pm", "languages" : null, "name" : "Admin Test Location", "phones_attributes" : [ { "number" : "7035551212", "vanity_number" : "703555-ABCD", "extension" : "x1223", "department" : "CalFresh" } ], "short_desc" : "This is a short description", "transportation" : "SAMTRANS stops within 1/2 mile.", "urls" : [ "http://codeforamerica.org" ], "services_attributes":[{"name":"Service for Admin Test Location","description":"just a test service","service_areas":["San Mateo County"]}] }] } diff --git a/spec/api/create_service_spec.rb b/spec/api/create_service_spec.rb index da4478ffb..b7bb4bec1 100644 --- a/spec/api/create_service_spec.rb +++ b/spec/api/create_service_spec.rb @@ -9,7 +9,9 @@ @service_attributes = { fees: 'new fees', audience: 'new audience', - keywords: %w(food youth) + keywords: %w(food youth), + name: 'test service', + description: 'test description' } end diff --git a/spec/api/get_location_services_spec.rb b/spec/api/get_location_services_spec.rb index 7e5035894..eaa60a318 100644 --- a/spec/api/get_location_services_spec.rb +++ b/spec/api/get_location_services_spec.rb @@ -41,7 +41,7 @@ end it 'includes the funding_sources attribute in the serialization' do - expect(json.first['funding_sources']).to eq([]) + expect(json.first['funding_sources']).to eq(['County']) end it 'includes the keywords attribute in the serialization' do @@ -57,7 +57,7 @@ end it 'includes the service_areas attribute in the serialization' do - expect(json.first['service_areas']).to eq([]) + expect(json.first['service_areas']).to eq(['Belmont']) end it 'includes the short_desc attribute in the serialization' do @@ -65,7 +65,7 @@ end it 'includes the urls attribute in the serialization' do - expect(json.first['urls']).to eq([]) + expect(json.first['urls']).to eq(['http://www.monfresh.com']) end it 'includes the wait attribute in the serialization' do diff --git a/spec/factories/services.rb b/spec/factories/services.rb index 484c682a6..06f36dccb 100644 --- a/spec/factories/services.rb +++ b/spec/factories/services.rb @@ -1,8 +1,9 @@ FactoryGirl.define do factory :service do - name 'Burlingame, Easton Branch' + name 'Literacy Program' description 'yoga classes' keywords ['library', 'food pantries', 'stood famps', 'emergency'] + location end factory :service_with_nil_fields, class: Service do @@ -10,6 +11,7 @@ description 'SNAP market' keywords %w(health yoga) fees nil + location end factory :service_with_extra_whitespace, class: Service do @@ -17,10 +19,14 @@ description ' SNAP market' eligibility ' seniors ' fees 'none ' + funding_sources ['County '] how_to_apply ' in person' keywords ['health ', ' yoga'] name 'Benefits ' short_desc 'processes applications ' + service_areas ['Belmont '] + urls [' http://www.monfresh.com '] wait '2 days ' + location end end diff --git a/spec/features/admin/services/create_service_spec.rb b/spec/features/admin/services/create_service_spec.rb new file mode 100644 index 000000000..184234841 --- /dev/null +++ b/spec/features/admin/services/create_service_spec.rb @@ -0,0 +1,137 @@ +require 'rails_helper' + +feature 'Create a new service' do + background do + create(:location) + login_super_admin + visit('/admin/locations/vrs-services') + click_link 'Add a new service' + end + + scenario 'with all required fields' do + fill_in 'service_name', with: 'New VRS Services service' + fill_in 'service_description', with: 'new description' + click_button 'Create service' + click_link 'New VRS Services service' + + expect(find_field('service_name').value).to eq 'New VRS Services service' + expect(find_field('service_description').value).to eq 'new description' + end + + scenario 'without any required fields' do + click_button 'Create service' + expect(page).to have_content "Description can't be blank for Service" + expect(page).to have_content "Name can't be blank for Service" + end + + scenario 'with audience' do + fill_in 'service_name', with: 'New VRS Services service' + fill_in 'service_description', with: 'new description' + fill_in 'service_audience', with: 'Low-income residents.' + click_button 'Create service' + click_link 'New VRS Services service' + + expect(find_field('service_audience').value).to eq 'Low-income residents.' + end + + scenario 'with eligibility' do + fill_in 'service_name', with: 'New VRS Services service' + fill_in 'service_description', with: 'new description' + fill_in 'service_eligibility', with: 'Low-income residents.' + click_button 'Create service' + click_link 'New VRS Services service' + + expect(find_field('service_eligibility').value).to eq 'Low-income residents.' + end + + scenario 'with fees' do + fill_in 'service_name', with: 'New VRS Services service' + fill_in 'service_description', with: 'new description' + fill_in 'service_fees', with: 'Low-income residents.' + click_button 'Create service' + click_link 'New VRS Services service' + + expect(find_field('service_fees').value).to eq 'Low-income residents.' + end + + scenario 'with how_to_apply' do + fill_in 'service_name', with: 'New VRS Services service' + fill_in 'service_description', with: 'new description' + fill_in 'service_how_to_apply', with: 'Low-income residents.' + click_button 'Create service' + click_link 'New VRS Services service' + + expect(find_field('service_how_to_apply').value).to eq 'Low-income residents.' + end + + scenario 'with wait' do + fill_in 'service_name', with: 'New VRS Services service' + fill_in 'service_description', with: 'new description' + fill_in 'service_wait', with: 'Low-income residents.' + click_button 'Create service' + click_link 'New VRS Services service' + + expect(find_field('service_wait').value).to eq 'Low-income residents.' + end + + scenario 'when adding a website', :js do + fill_in 'service_name', with: 'New VRS Services service' + fill_in 'service_description', with: 'new description' + click_link 'Add a website' + fill_in find(:xpath, "//input[@type='url']")[:id], with: 'http://ruby.com' + click_button 'Create service' + click_link 'New VRS Services service' + + expect(find_field('service[urls][]').value).to eq 'http://ruby.com' + end + + scenario 'when adding a keyword', :js do + fill_in 'service_name', with: 'New VRS Services service' + fill_in 'service_description', with: 'new description' + click_link 'Add a keyword' + fill_in find(:xpath, "//input[@name='service[keywords][]']")[:id], with: 'ruby' + click_button 'Create service' + click_link 'New VRS Services service' + + expect(find_field('service[keywords][]').value).to eq 'ruby' + end + + scenario 'when adding a service area', :js do + fill_in 'service_name', with: 'New VRS Services service' + fill_in 'service_description', with: 'new description' + click_link 'Add a service area' + fill_in find(:xpath, "//input[@name='service[service_areas][]']")[:id], with: 'Belmont' + click_button 'Create service' + click_link 'New VRS Services service' + + expect(find_field('service[service_areas][]').value).to eq 'Belmont' + end + + scenario 'when adding categories', :js do + emergency = Category.create!(name: 'Emergency', oe_id: '101') + emergency.children.create!(name: 'Disaster Response', oe_id: '101-01') + emergency.children.create!(name: 'Subcategory 2', oe_id: '101-02') + visit('/admin/locations/vrs-services/services/new') + + fill_in 'service_name', with: 'New VRS Services service' + fill_in 'service_description', with: 'new description' + check 'category_101' + check 'category_101-01' + click_button 'Create service' + click_link 'New VRS Services service' + + expect(find('#category_101-01')).to be_checked + expect(find('#category_101')).to be_checked + end +end + +describe 'when admin does not have access to the location' do + it 'denies access to create a new service' do + create(:location) + login_admin + + visit('/admin/locations/vrs-services/services/new') + + expect(page).to have_content "Sorry, you don't have access to that page." + end +end diff --git a/spec/features/admin/services/update_audience_spec.rb b/spec/features/admin/services/update_audience_spec.rb new file mode 100644 index 000000000..1eb040b3b --- /dev/null +++ b/spec/features/admin/services/update_audience_spec.rb @@ -0,0 +1,17 @@ +require 'rails_helper' + +feature 'Update audience' do + background do + create_service + login_super_admin + visit '/admin/locations/vrs-services' + end + + scenario 'with valid audience' do + click_link 'Literacy Program' + fill_in 'service_audience', with: 'Youth Counseling' + click_button 'Save changes' + expect(page).to have_content 'Service was successfully updated.' + expect(find_field('service_audience').value).to eq 'Youth Counseling' + end +end diff --git a/spec/features/admin/services/update_categories_spec.rb b/spec/features/admin/services/update_categories_spec.rb new file mode 100644 index 000000000..80f2b4e32 --- /dev/null +++ b/spec/features/admin/services/update_categories_spec.rb @@ -0,0 +1,43 @@ +require 'rails_helper' + +feature 'Update categories' do + background do + create_service + emergency = Category.create!(name: 'Emergency', oe_id: '101') + emergency.children.create!(name: 'Disaster Response', oe_id: '101-01') + emergency.children.create!(name: 'Subcategory 2', oe_id: '101-02') + emergency.children.create!(name: 'Subcategory 3', oe_id: '101-03') + + login_super_admin + visit '/admin/locations/vrs-services' + click_link 'Literacy Program' + end + + scenario 'when adding one subcategory', :js do + check 'category_101' + check 'category_101-01' + click_button 'Save changes' + + expect(find('#category_101')).to be_checked + expect(find('#category_101-01')).to be_checked + + uncheck 'category_101' + click_button 'Save changes' + + expect(find('#category_101')).to_not be_checked + end + + scenario 'when going to the 3rd subcategory', :js do + check 'category_101' + check 'category_101-01' + check 'category_101-02' + check 'category_101-03' + + click_button 'Save changes' + + expect(find('#category_101-03')).to be_checked + expect(find('#category_101-02')).to be_checked + expect(find('#category_101-01')).to be_checked + expect(find('#category_101')).to be_checked + end +end diff --git a/spec/features/admin/services/update_description_spec.rb b/spec/features/admin/services/update_description_spec.rb new file mode 100644 index 000000000..f9d68fd56 --- /dev/null +++ b/spec/features/admin/services/update_description_spec.rb @@ -0,0 +1,23 @@ +require 'rails_helper' + +feature 'Update description' do + background do + create_service + login_super_admin + visit '/admin/locations/vrs-services' + click_link 'Literacy Program' + end + + scenario 'with empty description' do + fill_in 'service_description', with: '' + click_button 'Save changes' + expect(page).to have_content "Description can't be blank for Service" + end + + scenario 'with valid description' do + fill_in 'service_description', with: 'Youth Counseling' + click_button 'Save changes' + expect(page).to have_content 'Service was successfully updated.' + expect(find_field('service_description').value).to eq 'Youth Counseling' + end +end diff --git a/spec/features/admin/services/update_eligibility_spec.rb b/spec/features/admin/services/update_eligibility_spec.rb new file mode 100644 index 000000000..561204a34 --- /dev/null +++ b/spec/features/admin/services/update_eligibility_spec.rb @@ -0,0 +1,17 @@ +require 'rails_helper' + +feature 'Update eligibility' do + background do + create_service + login_super_admin + visit '/admin/locations/vrs-services' + end + + scenario 'with valid eligibility' do + click_link 'Literacy Program' + fill_in 'service_eligibility', with: 'Youth Counseling' + click_button 'Save changes' + expect(page).to have_content 'Service was successfully updated.' + expect(find_field('service_eligibility').value).to eq 'Youth Counseling' + end +end diff --git a/spec/features/admin/services/update_fees_spec.rb b/spec/features/admin/services/update_fees_spec.rb new file mode 100644 index 000000000..c2f363a34 --- /dev/null +++ b/spec/features/admin/services/update_fees_spec.rb @@ -0,0 +1,17 @@ +require 'rails_helper' + +feature 'Update fees' do + background do + create_service + login_super_admin + visit '/admin/locations/vrs-services' + end + + scenario 'with valid fees' do + click_link 'Literacy Program' + fill_in 'service_fees', with: 'Youth Counseling' + click_button 'Save changes' + expect(page).to have_content 'Service was successfully updated.' + expect(find_field('service_fees').value).to eq 'Youth Counseling' + end +end diff --git a/spec/features/admin/services/update_how_to_apply_spec.rb b/spec/features/admin/services/update_how_to_apply_spec.rb new file mode 100644 index 000000000..6db86c18b --- /dev/null +++ b/spec/features/admin/services/update_how_to_apply_spec.rb @@ -0,0 +1,17 @@ +require 'rails_helper' + +feature 'Update how_to_apply' do + background do + create_service + login_super_admin + visit '/admin/locations/vrs-services' + end + + scenario 'with valid how_to_apply' do + click_link 'Literacy Program' + fill_in 'service_how_to_apply', with: 'Youth Counseling' + click_button 'Save changes' + expect(page).to have_content 'Service was successfully updated.' + expect(find_field('service_how_to_apply').value).to eq 'Youth Counseling' + end +end diff --git a/spec/features/admin/services/update_keywords_spec.rb b/spec/features/admin/services/update_keywords_spec.rb new file mode 100644 index 000000000..40508b7ce --- /dev/null +++ b/spec/features/admin/services/update_keywords_spec.rb @@ -0,0 +1,45 @@ +require 'rails_helper' + +feature 'Update keywords' do + background do + create_service + login_super_admin + end + + scenario 'when no keywords exist' do + @service.update!(keywords: []) + visit '/admin/locations/vrs-services' + click_link 'Literacy Program' + expect(page).to have_no_xpath("//input[@name='service[keywords][]']") + end + + scenario 'by adding 2 new keywords', :js do + @service.update!(keywords: []) + visit '/admin/locations/vrs-services' + click_link 'Literacy Program' + add_two_keywords + expect(find_field('service_keywords_0').value).to eq 'homeless' + delete_all_keywords + expect(page).to have_no_xpath("//input[@name='service[keywords][]']") + end + + scenario 'with 2 keywords but one is empty', :js do + @service.update!(keywords: ['education']) + visit '/admin/locations/vrs-services' + click_link 'Literacy Program' + click_link 'Add a keyword' + click_button 'Save changes' + total_keywords = all(:xpath, "//input[@name='service[keywords][]']") + expect(total_keywords.length).to eq 1 + end + + scenario 'with valid keyword' do + @service.update!(keywords: ['health']) + visit '/admin/locations/vrs-services' + click_link 'Literacy Program' + fill_in 'service_keywords_0', with: 'food pantry' + click_button 'Save changes' + expect(find_field('service_keywords_0').value). + to eq 'food pantry' + end +end diff --git a/spec/features/admin/services/update_name_spec.rb b/spec/features/admin/services/update_name_spec.rb new file mode 100644 index 000000000..e846f4018 --- /dev/null +++ b/spec/features/admin/services/update_name_spec.rb @@ -0,0 +1,23 @@ +require 'rails_helper' + +feature 'Update name' do + background do + create_service + login_super_admin + visit '/admin/locations/vrs-services' + click_link 'Literacy Program' + end + + scenario 'with empty name' do + fill_in 'service_name', with: '' + click_button 'Save changes' + expect(page).to have_content "Name can't be blank for Service" + end + + scenario 'with valid name' do + fill_in 'service_name', with: 'Youth Counseling' + click_button 'Save changes' + expect(page).to have_content 'Service was successfully updated.' + expect(find_field('service_name').value).to eq 'Youth Counseling' + end +end diff --git a/spec/features/admin/services/update_service_areas_spec.rb b/spec/features/admin/services/update_service_areas_spec.rb new file mode 100644 index 000000000..2e8cd9f35 --- /dev/null +++ b/spec/features/admin/services/update_service_areas_spec.rb @@ -0,0 +1,60 @@ +require 'rails_helper' + +feature 'Update service areas' do + background do + create_service + login_super_admin + visit '/admin/locations/vrs-services' + click_link 'Literacy Program' + end + + scenario 'when no service areas exist' do + expect(page).to have_no_xpath("//input[@name='service[service_areas][]']") + end + + scenario 'by adding 2 new service areas', :js do + add_two_service_areas + expect(find_field('service_service_areas_0').value).to eq 'Belmont' + delete_all_service_areas + expect(page).to have_no_xpath("//input[@name='service[service_areas][]']") + end + + scenario 'with 2 service_areas but one is empty', :js do + @service.update!(service_areas: ['Belmont']) + visit '/admin/locations/vrs-services' + click_link 'Literacy Program' + click_link 'Add a service area' + click_button 'Save changes' + total_service_areas = all(:xpath, "//input[@name='service[service_areas][]']") + expect(total_service_areas.length).to eq 1 + end + + scenario 'with invalid service area' do + @service.update!(service_areas: ['Belmont']) + visit '/admin/locations/vrs-services' + click_link 'Literacy Program' + fill_in 'service_service_areas_0', with: 'Fairfax' + click_button 'Save changes' + expect(page). + to have_content 'At least one service area is improperly formatted' + end + + scenario 'with valid service area' do + @service.update!(service_areas: ['Belmont']) + visit '/admin/locations/vrs-services' + click_link 'Literacy Program' + fill_in 'service_service_areas_0', with: 'Atherton' + click_button 'Save changes' + expect(find_field('service_service_areas_0').value). + to eq 'Atherton' + end + + scenario 'clearing out existing service area' do + @service.update!(service_areas: ['Belmont']) + visit '/admin/locations/vrs-services' + click_link 'Literacy Program' + fill_in 'service_service_areas_0', with: '' + click_button 'Save changes' + expect(page).not_to have_field('service_service_areas_0') + end +end diff --git a/spec/features/admin/services/update_urls_spec.rb b/spec/features/admin/services/update_urls_spec.rb new file mode 100644 index 000000000..bb85eff0e --- /dev/null +++ b/spec/features/admin/services/update_urls_spec.rb @@ -0,0 +1,50 @@ +require 'rails_helper' + +feature 'Update websites' do + background do + create_service + login_super_admin + visit '/admin/locations/vrs-services' + click_link 'Literacy Program' + end + + scenario 'when no websites exist' do + expect(page).to have_no_xpath("//input[@name='service[urls][]']") + end + + scenario 'by adding 2 new websites', :js do + add_two_urls + expect(find_field('service_urls_0').value).to eq 'http://ruby.com' + delete_all_urls + expect(page).to have_no_xpath("//input[@name='service[urls][]']") + end + + scenario 'with 2 urls but one is empty', :js do + @service.update!(urls: ['http://ruby.org']) + visit '/admin/locations/vrs-services' + click_link 'Literacy Program' + click_link 'Add a website' + click_button 'Save changes' + total_urls = all(:xpath, "//input[@type='url']") + expect(total_urls.length).to eq 1 + end + + scenario 'with invalid website' do + @service.update!(urls: ['http://ruby.org']) + visit '/admin/locations/vrs-services' + click_link 'Literacy Program' + fill_in 'service_urls_0', with: 'www.monfresh.com' + click_button 'Save changes' + expect(page).to have_content 'www.monfresh.com is not a valid URL' + end + + scenario 'with valid website' do + @service.update!(urls: ['http://ruby.org']) + visit '/admin/locations/vrs-services' + click_link 'Literacy Program' + fill_in 'service_urls_0', with: 'http://codeforamerica.org' + click_button 'Save changes' + expect(find_field('service_urls_0').value). + to eq 'http://codeforamerica.org' + end +end diff --git a/spec/features/admin/services/update_wait_time_spec.rb b/spec/features/admin/services/update_wait_time_spec.rb new file mode 100644 index 000000000..ebf128967 --- /dev/null +++ b/spec/features/admin/services/update_wait_time_spec.rb @@ -0,0 +1,17 @@ +require 'rails_helper' + +feature 'Update wait' do + background do + create_service + login_super_admin + visit '/admin/locations/vrs-services' + end + + scenario 'with valid wait' do + click_link 'Literacy Program' + fill_in 'service_wait', with: 'Youth Counseling' + click_button 'Save changes' + expect(page).to have_content 'Service was successfully updated.' + expect(find_field('service_wait').value).to eq 'Youth Counseling' + end +end diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index 2ad37eb3d..8dccc6e8b 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -20,6 +20,10 @@ it { is_expected.to belong_to(:location).touch(true) } + it { is_expected.to validate_presence_of(:name).with_message("can't be blank for Service") } + it { is_expected.to validate_presence_of(:description).with_message("can't be blank for Service") } + it { is_expected.to validate_presence_of(:location).with_message("can't be blank for Service") } + # This is no longer working in Rails 4.1.2. I opened an issue: # https://github.com/thoughtbot/shoulda-matchers/issues/549 xit { is_expected.to have_and_belong_to_many(:categories).order('oe_id asc') } @@ -53,16 +57,6 @@ ) end - it do - is_expected.not_to allow_value(['Belmont ']). - for(:service_areas). - with_message( - 'At least one service area is improperly formatted, ' \ - 'or is not an accepted city or county name. Please make sure all ' \ - 'words are capitalized.' - ) - end - it { is_expected.to allow_value(%w(Belmont Atherton)).for(:service_areas) } describe 'auto_strip_attributes' do @@ -73,10 +67,13 @@ expect(service.description).to eq('SNAP market') expect(service.eligibility).to eq('seniors') expect(service.fees).to eq('none') + expect(service.funding_sources).to eq(['County']) expect(service.how_to_apply).to eq('in person') expect(service.keywords).to eq(%w(health yoga)) expect(service.name).to eq('Benefits') expect(service.short_desc).to eq('processes applications') + expect(service.service_areas).to eq(['Belmont']) + expect(service.urls).to eq(['http://www.monfresh.com']) expect(service.wait).to eq('2 days') end end diff --git a/spec/support/features/form_helpers.rb b/spec/support/features/form_helpers.rb index 5e7d8ff40..ff54ab0d8 100644 --- a/spec/support/features/form_helpers.rb +++ b/spec/support/features/form_helpers.rb @@ -170,5 +170,37 @@ def fill_in_all_required_fields fill_in 'location_address_attributes_state', with: 'CA' fill_in 'location_address_attributes_zip', with: '12345' end + + def add_two_keywords + click_link 'Add a keyword' + fill_in 'service[keywords][]', with: 'homeless' + click_link 'Add a keyword' + keywords = page. + all(:xpath, "//input[@name='service[keywords][]']") + fill_in keywords[-1][:id], with: 'CalFresh' + click_button 'Save changes' + end + + def delete_all_keywords + find_link('Delete this keyword permanently', match: :first).click + find_link('Delete this keyword permanently', match: :first).click + click_button 'Save changes' + end + + def add_two_service_areas + click_link 'Add a service area' + fill_in 'service[service_areas][]', with: 'Belmont' + click_link 'Add a service area' + service_areas = page. + all(:xpath, "//input[@name='service[service_areas][]']") + fill_in service_areas[-1][:id], with: 'Atherton' + click_button 'Save changes' + end + + def delete_all_service_areas + find_link('Delete this service area permanently', match: :first).click + find_link('Delete this service area permanently', match: :first).click + click_button 'Save changes' + end end end From c1427a729719d54ab1e60dc18d202c73b6162ea0 Mon Sep 17 00:00:00 2001 From: Moncef Belyamani Date: Tue, 15 Jul 2014 10:55:04 -0400 Subject: [PATCH 16/17] Increase Capybara wait time for Travis. --- spec/rails_helper.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index c59ea5bf4..47208e3ba 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -10,6 +10,7 @@ require 'capybara/poltergeist' Capybara.javascript_driver = :poltergeist +Capybara.default_wait_time = 30 Rails.logger.level = 4 From dcda00329d98ad24f387e1e6c034fe5a696c41da Mon Sep 17 00:00:00 2001 From: Moncef Belyamani Date: Tue, 15 Jul 2014 11:23:06 -0400 Subject: [PATCH 17/17] Update documentation for admin_support_email setting. --- config/settings.example.yml | 5 ++++- config/settings.yml | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/config/settings.example.yml b/config/settings.example.yml index 3d6aef0f4..cc76cd6cc 100644 --- a/config/settings.example.yml +++ b/config/settings.example.yml @@ -61,7 +61,10 @@ generic_domains: - hotmail.com - yahoo.com -# The email that admin interface users should send questions/issues to. +# The email address that admin interface users should send questions/issues to. +# This link appears when an admin views their locations and organizations. +# See app/views/admin/locations/index.html.haml and +# app/views/admin/organizations/index.html.haml. admin_support_email: ######################### diff --git a/config/settings.yml b/config/settings.yml index 69c62d96d..3f2639f48 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -61,7 +61,10 @@ generic_domains: - hotmail.com - yahoo.com -# The email that admin interface users should send questions/issues to. +# The email address that admin interface users should send questions/issues to. +# This link appears when an admin views their locations and organizations. +# See app/views/admin/locations/index.html.haml and +# app/views/admin/organizations/index.html.haml. admin_support_email: ohanapi@codeforamerica.org #########################