├── .ctags
├── .gitignore
├── .hound.yml
├── .rspec
├── .ruby-version
├── .sample.env
├── .scss_ignore
├── .scss_style.yml
├── Gemfile
├── Gemfile.lock
├── Procfile
├── README.md
├── Rakefile
├── app.json
├── app
├── assets
│ ├── images
│ │ ├── default_cover.jpg
│ │ └── logo_symbol.png
│ ├── javascripts
│ │ └── application.js
│ └── stylesheets
│ │ ├── application.scss
│ │ ├── base
│ │ ├── _base.scss
│ │ ├── _buttons.scss
│ │ ├── _forms.scss
│ │ ├── _grid-settings.scss
│ │ ├── _lists.scss
│ │ ├── _tables.scss
│ │ ├── _typography.scss
│ │ └── _variables.scss
│ │ ├── layout
│ │ ├── _booklist.scss
│ │ ├── _default_list.scss
│ │ └── _layout.scss
│ │ └── refills
│ │ ├── _comments.scss
│ │ ├── _flashes.scss
│ │ ├── _footer.scss
│ │ ├── _header.scss
│ │ └── _hero.scss
├── controllers
│ ├── application_controller.rb
│ ├── authors_controller.rb
│ ├── books
│ │ └── filter_controller.rb
│ ├── books_controller.rb
│ ├── concerns
│ │ └── authentication.rb
│ ├── lists_controller.rb
│ ├── pages_controller.rb
│ ├── registrations_controller.rb
│ ├── reviews_controller.rb
│ └── sessions_controller.rb
├── facades
│ └── book_facade.rb
├── forms
│ └── filter.rb
├── helpers
│ ├── application_helper.rb
│ ├── books_helper.rb
│ └── flashes_helper.rb
├── mailers
│ └── .keep
├── models
│ ├── author.rb
│ ├── book.rb
│ ├── concerns
│ │ └── .keep
│ ├── genre.rb
│ ├── guest.rb
│ ├── list.rb
│ ├── list_entry.rb
│ ├── review.rb
│ └── user.rb
├── policies
│ └── review_policy.rb
└── views
│ ├── application
│ ├── _analytics.html.erb
│ ├── _flashes.html.erb
│ ├── _footer.html.erb
│ ├── _header.html.erb
│ └── _javascript.html.erb
│ ├── authors
│ ├── index.html.erb
│ └── show.html.erb
│ ├── books
│ ├── _books.html.erb
│ ├── _details.html.erb
│ ├── _filter.html.erb
│ ├── _reviews.html.erb
│ ├── index.html.erb
│ └── show.html.erb
│ ├── layouts
│ └── application.html.erb
│ ├── lists
│ ├── _list.html.erb
│ ├── index.html.erb
│ └── show.html.erb
│ ├── pages
│ └── home.html.erb
│ ├── registrations
│ └── new.html.erb
│ ├── reviews
│ ├── _form.html.erb
│ └── _review.html.erb
│ ├── sessions
│ └── new.html.erb
│ └── shared
│ └── _registration_form_fields.html.erb
├── bin
├── bundle
├── delayed_job
├── rails
├── rake
├── rspec
├── setup
├── setup-pr-review
└── spring
├── browserslist
├── circle.yml
├── config.ru
├── config
├── application.rb
├── boot.rb
├── database.yml
├── environment.rb
├── environments
│ ├── development.rb
│ ├── production.rb
│ ├── staging.rb
│ └── test.rb
├── i18n-tasks.yml
├── initializers
│ ├── assets.rb
│ ├── backtrace_silencers.rb
│ ├── cookies_serializer.rb
│ ├── disable_xml_params.rb
│ ├── errors.rb
│ ├── filter_parameter_logging.rb
│ ├── friendly_id.rb
│ ├── inflections.rb
│ ├── json_encoding.rb
│ ├── kaminari_config.rb
│ ├── mime_types.rb
│ ├── session_store.rb
│ ├── simple_form.rb
│ └── wrap_parameters.rb
├── locales
│ ├── en.yml
│ └── simple_form.en.yml
├── newrelic.yml
├── puma.rb
├── routes.rb
├── secrets.yml
└── smtp.rb
├── db
├── migrate
│ ├── 20151024110515_create_delayed_jobs.rb
│ ├── 20151024161110_create_books.rb
│ ├── 20151025083545_add_summary_to_books.rb
│ ├── 20151025170319_create_genres.rb
│ ├── 20151025170953_add_genre_to_books.rb
│ ├── 20151026202920_create_lists.rb
│ ├── 20151026205814_create_list_entries.rb
│ ├── 20151027020720_create_authors.rb
│ ├── 20151111024028_add_description_to_author.rb
│ ├── 20151111024650_add_author_to_books.rb
│ ├── 20151111033154_add_cover_to_book.rb
│ ├── 20151111234610_create_users.rb
│ ├── 20151114125209_create_friendly_id_slugs.rb
│ ├── 20151114145753_add_released_on_to_books.rb
│ ├── 20151115085750_create_reviews.rb
│ └── 20151115205953_add_rating_to_reviews.rb
├── schema.rb
└── seeds.rb
├── lib
├── tasks
│ ├── add_slugs_to_existing_records.rake
│ ├── bundler_audit.rake
│ └── dev.rake
└── templates
│ └── erb
│ └── scaffold
│ └── _form.html.erb
├── log
└── .keep
├── public
├── 404.html
├── 422.html
├── 500.html
├── favicon.ico
└── robots.txt
└── spec
├── controllers
├── registrations_controller_spec.rb
├── reviews_controller_spec.rb
└── sessions_controller_spec.rb
├── facades
└── book_facade_spec.rb
├── factories.rb
├── features
├── filter_books_by_genre_spec.rb
├── pagination_spec.rb
├── show_all_of_the_lists_of_books_spec.rb
├── sort_by_date_spec.rb
├── user_logs_in_spec.rb
├── user_logs_out_spec.rb
├── user_writes_a_review_spec.rb
├── view_a_single_book_spec.rb
├── view_an_individual_author_spec.rb
├── view_an_individual_list_spec.rb
├── view_list_of_all_books_spec.rb
├── view_list_of_authors_spec.rb
└── visitor_creates_account_spec.rb
├── forms
└── filter_spec.rb
├── helpers
└── books_helper_spec.rb
├── i18n_spec.rb
├── models
├── author_spec.rb
├── book_spec.rb
├── genre_spec.rb
├── guest_spec.rb
├── list_spec.rb
├── review_spec.rb
└── user_spec.rb
├── policies
└── review_policy_spec.rb
├── rails_helper.rb
├── spec_helper.rb
├── support
├── action_mailer.rb
├── database_cleaner.rb
├── factory_girl.rb
├── features
│ ├── book_helpers.rb
│ └── login_helpers.rb
├── i18n.rb
├── matchers
│ └── .keep
├── mixins
│ └── .keep
├── shared_examples
│ └── .keep
└── shoulda_matchers.rb
└── views
├── authors
├── index.html.erb_spec.rb
└── show.html.erb_spec.rb
├── books
├── _details.html.erb_spec.rb
└── index.html.erb_spec.rb
├── lists
├── _list.html.erb_spec.rb
├── index.html.erb_spec.rb
└── show.html.erb_spec.rb
└── reviews
└── _review.html.erb_spec.rb
/.ctags:
--------------------------------------------------------------------------------
1 | --recurse=yes
2 | --exclude=vendor
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | !.keep
2 | *.DS_Store
3 | *.swo
4 | *.swp
5 | /.bundle
6 | /.env
7 | /.foreman
8 | /coverage/*
9 | /db/*.sqlite3
10 | /log/*
11 | /public/system
12 | /public/assets
13 | /tags
14 | /tmp/*
15 |
--------------------------------------------------------------------------------
/.hound.yml:
--------------------------------------------------------------------------------
1 | # See https://houndci.com/configuration for help.
2 | coffeescript:
3 | # config_file: .coffeescript-style.json
4 | enabled: true
5 | haml:
6 | # config_file: .haml-style.yml
7 | enabled: true
8 | javascript:
9 | # config_file: .javascript-style.json
10 | enabled: true
11 | # ignore_file: .javascript_ignore
12 | ruby:
13 | # config_file: .ruby-style.yml
14 | enabled: true
15 | scss:
16 | config_file: .scss-style.yml
17 | ignore_file: .scss_ignore
18 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | --require spec_helper
3 |
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | 2.2.3
2 |
--------------------------------------------------------------------------------
/.sample.env:
--------------------------------------------------------------------------------
1 | # http://ddollar.github.com/foreman/
2 | ASSET_HOST=localhost:3000
3 | APPLICATION_HOST=localhost:3000
4 | RACK_ENV=development
5 | SECRET_KEY_BASE=development_secret
6 | EXECJS_RUNTIME=Node
7 | SMTP_ADDRESS=smtp.example.com
8 | SMTP_DOMAIN=example.com
9 | SMTP_PASSWORD=password
10 | SMTP_USERNAME=username
11 |
--------------------------------------------------------------------------------
/.scss_ignore:
--------------------------------------------------------------------------------
1 | app/assets/stylesheets/refills/*.scss
2 |
--------------------------------------------------------------------------------
/.scss_style.yml:
--------------------------------------------------------------------------------
1 | scss_files: "**/*.scss"
2 | linters:
3 | BangFormat:
4 | enabled: true
5 | space_before_bang: true
6 | space_after_bang: false
7 | BorderZero:
8 | enabled: false
9 | convention: zero
10 | ColorKeyword:
11 | enabled: true
12 | severity: warning
13 | ColorVariable:
14 | enabled: true
15 | Comment:
16 | enabled: true
17 | DebugStatement:
18 | enabled: true
19 | DeclarationOrder:
20 | enabled: true
21 | DuplicateProperty:
22 | enabled: true
23 | ElsePlacement:
24 | enabled: true
25 | style: same_line
26 | EmptyLineBetweenBlocks:
27 | enabled: true
28 | ignore_single_line_blocks: true
29 | EmptyRule:
30 | enabled: true
31 | FinalNewline:
32 | enabled: true
33 | present: true
34 | HexLength:
35 | enabled: false
36 | style: short
37 | HexNotation:
38 | enabled: true
39 | style: lowercase
40 | HexValidation:
41 | enabled: true
42 | IdSelector:
43 | enabled: true
44 | ImportantRule:
45 | enabled: true
46 | ImportPath:
47 | enabled: true
48 | leading_underscore: false
49 | filename_extension: false
50 | Indentation:
51 | enabled: true
52 | allow_non_nested_indentation: false
53 | character: space
54 | width: 2
55 | LeadingZero:
56 | enabled: true
57 | style: include_zero
58 | MergeableSelector:
59 | enabled: true
60 | force_nesting: true
61 | NameFormat:
62 | enabled: true
63 | allow_leading_underscore: true
64 | convention: hyphenated_lowercase
65 | NestingDepth:
66 | enabled: true
67 | max_depth: 4
68 | severity: warning
69 | PlaceholderInExtend:
70 | enabled: false
71 | PropertyCount:
72 | enabled: true
73 | include_nested: false
74 | max_properties: 10
75 | PropertySortOrder:
76 | enabled: true
77 | ignore_unspecified: false
78 | severity: warning
79 | separate_groups: false
80 | PropertySpelling:
81 | enabled: true
82 | extra_properties: []
83 | QualifyingElement:
84 | enabled: true
85 | allow_element_with_attribute: false
86 | allow_element_with_class: false
87 | allow_element_with_id: false
88 | severity: warning
89 | SelectorDepth:
90 | enabled: true
91 | max_depth: 3
92 | severity: warning
93 | SelectorFormat:
94 | enabled: true
95 | convention: hyphenated_lowercase
96 | Shorthand:
97 | enabled: true
98 | severity: warning
99 | SingleLinePerProperty:
100 | enabled: true
101 | allow_single_line_rule_sets: true
102 | SingleLinePerSelector:
103 | enabled: true
104 | SpaceAfterComma:
105 | enabled: true
106 | SpaceAfterPropertyColon:
107 | enabled: true
108 | style: one_space
109 | SpaceAfterPropertyName:
110 | enabled: true
111 | SpaceBeforeBrace:
112 | enabled: true
113 | style: space
114 | allow_single_line_padding: false
115 | SpaceBetweenParens:
116 | enabled: true
117 | spaces: 0
118 | StringQuotes:
119 | enabled: true
120 | style: double_quotes
121 | TrailingSemicolon:
122 | enabled: true
123 | TrailingZero:
124 | enabled: false
125 | UnnecessaryMantissa:
126 | enabled: true
127 | UnnecessaryParentReference:
128 | enabled: true
129 | UrlFormat:
130 | enabled: true
131 | UrlQuotes:
132 | enabled: true
133 | VariableForProperty:
134 | enabled: false
135 | properties: []
136 | VendorPrefixes:
137 | enabled: true
138 | identifier_list: bourbon
139 | include: []
140 | exclude: []
141 | ZeroUnit:
142 | enabled: true
143 | severity: warning
144 | Compass::PropertyWithMixin:
145 | enabled: false
146 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | ruby "2.2.3"
4 |
5 | gem "airbrake"
6 | gem "autoprefixer-rails"
7 | gem "bcrypt", "~> 3.1.7"
8 | gem "bourbon", "~> 4.2.0"
9 | gem "coffee-rails", "~> 4.1.0"
10 | gem "delayed_job_active_record"
11 | gem "email_validator"
12 | gem "flutie"
13 | gem "friendly_id"
14 | gem "high_voltage"
15 | gem "jquery-rails"
16 | gem "kaminari"
17 | gem "neat", "~> 1.7.0"
18 | gem "newrelic_rpm", ">= 3.9.8"
19 | gem "normalize-rails", "~> 3.0.0"
20 | gem "pg"
21 | gem "puma"
22 | gem "rack-canonical-host"
23 | gem "rails", "~> 4.2.0"
24 | gem "recipient_interceptor"
25 | gem "sass-rails", "~> 5.0"
26 | gem "simple_form"
27 | gem "title"
28 | gem "uglifier"
29 | gem 'gravatar_image_tag', '~> 1.2'
30 |
31 | group :development do
32 | gem "quiet_assets"
33 | gem "refills"
34 | gem "spring"
35 | gem "spring-commands-rspec"
36 | gem "web-console"
37 | end
38 |
39 | group :development, :test do
40 | gem "awesome_print"
41 | gem "bundler-audit", require: false
42 | gem "byebug"
43 | gem "dotenv-rails"
44 | gem "factory_girl_rails"
45 | gem "i18n-tasks"
46 | gem "pry-rails"
47 | gem "rspec-rails", "~> 3.3.0"
48 | end
49 |
50 | group :test do
51 | gem "capybara-webkit"
52 | gem "database_cleaner"
53 | gem "formulaic"
54 | gem "launchy"
55 | gem "shoulda-matchers"
56 | gem "simplecov", require: false
57 | gem "timecop"
58 | gem "webmock"
59 | end
60 |
61 | group :staging, :production do
62 | gem "rack-timeout"
63 | end
64 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | actionmailer (4.2.4)
5 | actionpack (= 4.2.4)
6 | actionview (= 4.2.4)
7 | activejob (= 4.2.4)
8 | mail (~> 2.5, >= 2.5.4)
9 | rails-dom-testing (~> 1.0, >= 1.0.5)
10 | actionpack (4.2.4)
11 | actionview (= 4.2.4)
12 | activesupport (= 4.2.4)
13 | rack (~> 1.6)
14 | rack-test (~> 0.6.2)
15 | rails-dom-testing (~> 1.0, >= 1.0.5)
16 | rails-html-sanitizer (~> 1.0, >= 1.0.2)
17 | actionview (4.2.4)
18 | activesupport (= 4.2.4)
19 | builder (~> 3.1)
20 | erubis (~> 2.7.0)
21 | rails-dom-testing (~> 1.0, >= 1.0.5)
22 | rails-html-sanitizer (~> 1.0, >= 1.0.2)
23 | activejob (4.2.4)
24 | activesupport (= 4.2.4)
25 | globalid (>= 0.3.0)
26 | activemodel (4.2.4)
27 | activesupport (= 4.2.4)
28 | builder (~> 3.1)
29 | activerecord (4.2.4)
30 | activemodel (= 4.2.4)
31 | activesupport (= 4.2.4)
32 | arel (~> 6.0)
33 | activesupport (4.2.4)
34 | i18n (~> 0.7)
35 | json (~> 1.7, >= 1.7.7)
36 | minitest (~> 5.1)
37 | thread_safe (~> 0.3, >= 0.3.4)
38 | tzinfo (~> 1.1)
39 | addressable (2.3.8)
40 | airbrake (4.3.3)
41 | builder
42 | multi_json
43 | arel (6.0.3)
44 | autoprefixer-rails (6.0.3)
45 | execjs
46 | json
47 | awesome_print (1.6.1)
48 | bcrypt (3.1.10)
49 | binding_of_caller (0.7.2)
50 | debug_inspector (>= 0.0.1)
51 | bourbon (4.2.6)
52 | sass (~> 3.4)
53 | thor (~> 0.19)
54 | builder (3.2.2)
55 | bundler-audit (0.4.0)
56 | bundler (~> 1.2)
57 | thor (~> 0.18)
58 | byebug (6.0.2)
59 | capybara (2.5.0)
60 | mime-types (>= 1.16)
61 | nokogiri (>= 1.3.3)
62 | rack (>= 1.0.0)
63 | rack-test (>= 0.5.4)
64 | xpath (~> 2.0)
65 | capybara-webkit (1.7.1)
66 | capybara (>= 2.3.0, < 2.6.0)
67 | json
68 | coderay (1.1.0)
69 | coffee-rails (4.1.0)
70 | coffee-script (>= 2.2.0)
71 | railties (>= 4.0.0, < 5.0)
72 | coffee-script (2.4.1)
73 | coffee-script-source
74 | execjs
75 | coffee-script-source (1.9.1.1)
76 | crack (0.4.2)
77 | safe_yaml (~> 1.0.0)
78 | database_cleaner (1.5.1)
79 | debug_inspector (0.0.2)
80 | delayed_job (4.1.1)
81 | activesupport (>= 3.0, < 5.0)
82 | delayed_job_active_record (4.1.0)
83 | activerecord (>= 3.0, < 5)
84 | delayed_job (>= 3.0, < 5)
85 | diff-lcs (1.2.5)
86 | docile (1.1.5)
87 | dotenv (2.0.2)
88 | dotenv-rails (2.0.2)
89 | dotenv (= 2.0.2)
90 | railties (~> 4.0)
91 | easy_translate (0.5.0)
92 | json
93 | thread
94 | thread_safe
95 | email_validator (1.6.0)
96 | activemodel
97 | erubis (2.7.0)
98 | execjs (2.6.0)
99 | factory_girl (4.5.0)
100 | activesupport (>= 3.0.0)
101 | factory_girl_rails (4.5.0)
102 | factory_girl (~> 4.5.0)
103 | railties (>= 3.0.0)
104 | flutie (2.0.0)
105 | formulaic (0.3.0)
106 | activesupport
107 | capybara
108 | i18n
109 | friendly_id (5.1.0)
110 | activerecord (>= 4.0.0)
111 | globalid (0.3.6)
112 | activesupport (>= 4.1.0)
113 | gravatar_image_tag (1.2.0)
114 | hashdiff (0.2.2)
115 | high_voltage (2.4.0)
116 | highline (1.7.8)
117 | i18n (0.7.0)
118 | i18n-tasks (0.8.7)
119 | activesupport (>= 2.3.18)
120 | easy_translate (>= 0.5.0)
121 | erubis
122 | highline (>= 1.7.3)
123 | i18n
124 | term-ansicolor (>= 1.3.2)
125 | terminal-table (>= 1.5.1)
126 | jquery-rails (4.0.5)
127 | rails-dom-testing (~> 1.0)
128 | railties (>= 4.2.0)
129 | thor (>= 0.14, < 2.0)
130 | json (1.8.3)
131 | kaminari (0.16.3)
132 | actionpack (>= 3.0.0)
133 | activesupport (>= 3.0.0)
134 | launchy (2.4.3)
135 | addressable (~> 2.3)
136 | loofah (2.0.3)
137 | nokogiri (>= 1.5.9)
138 | mail (2.6.3)
139 | mime-types (>= 1.16, < 3)
140 | method_source (0.8.2)
141 | mime-types (2.6.2)
142 | mini_portile (0.6.2)
143 | minitest (5.8.1)
144 | multi_json (1.11.2)
145 | neat (1.7.2)
146 | bourbon (>= 4.0)
147 | sass (>= 3.3)
148 | newrelic_rpm (3.14.0.305)
149 | nokogiri (1.6.6.2)
150 | mini_portile (~> 0.6.0)
151 | normalize-rails (3.0.3)
152 | pg (0.18.3)
153 | pry (0.10.3)
154 | coderay (~> 1.1.0)
155 | method_source (~> 0.8.1)
156 | slop (~> 3.4)
157 | pry-rails (0.3.4)
158 | pry (>= 0.9.10)
159 | puma (2.14.0)
160 | quiet_assets (1.1.0)
161 | railties (>= 3.1, < 5.0)
162 | rack (1.6.4)
163 | rack-canonical-host (0.1.0)
164 | addressable
165 | rack (~> 1.0)
166 | rack-test (0.6.3)
167 | rack (>= 1.0)
168 | rack-timeout (0.3.2)
169 | rails (4.2.4)
170 | actionmailer (= 4.2.4)
171 | actionpack (= 4.2.4)
172 | actionview (= 4.2.4)
173 | activejob (= 4.2.4)
174 | activemodel (= 4.2.4)
175 | activerecord (= 4.2.4)
176 | activesupport (= 4.2.4)
177 | bundler (>= 1.3.0, < 2.0)
178 | railties (= 4.2.4)
179 | sprockets-rails
180 | rails-deprecated_sanitizer (1.0.3)
181 | activesupport (>= 4.2.0.alpha)
182 | rails-dom-testing (1.0.7)
183 | activesupport (>= 4.2.0.beta, < 5.0)
184 | nokogiri (~> 1.6.0)
185 | rails-deprecated_sanitizer (>= 1.0.1)
186 | rails-html-sanitizer (1.0.2)
187 | loofah (~> 2.0)
188 | railties (4.2.4)
189 | actionpack (= 4.2.4)
190 | activesupport (= 4.2.4)
191 | rake (>= 0.8.7)
192 | thor (>= 0.18.1, < 2.0)
193 | rake (10.4.2)
194 | recipient_interceptor (0.1.2)
195 | mail
196 | refills (0.1.0)
197 | rspec-core (3.3.2)
198 | rspec-support (~> 3.3.0)
199 | rspec-expectations (3.3.1)
200 | diff-lcs (>= 1.2.0, < 2.0)
201 | rspec-support (~> 3.3.0)
202 | rspec-mocks (3.3.2)
203 | diff-lcs (>= 1.2.0, < 2.0)
204 | rspec-support (~> 3.3.0)
205 | rspec-rails (3.3.3)
206 | actionpack (>= 3.0, < 4.3)
207 | activesupport (>= 3.0, < 4.3)
208 | railties (>= 3.0, < 4.3)
209 | rspec-core (~> 3.3.0)
210 | rspec-expectations (~> 3.3.0)
211 | rspec-mocks (~> 3.3.0)
212 | rspec-support (~> 3.3.0)
213 | rspec-support (3.3.0)
214 | safe_yaml (1.0.4)
215 | sass (3.4.19)
216 | sass-rails (5.0.4)
217 | railties (>= 4.0.0, < 5.0)
218 | sass (~> 3.1)
219 | sprockets (>= 2.8, < 4.0)
220 | sprockets-rails (>= 2.0, < 4.0)
221 | tilt (>= 1.1, < 3)
222 | shoulda-matchers (3.0.1)
223 | activesupport (>= 4.0.0)
224 | simple_form (3.2.0)
225 | actionpack (~> 4.0)
226 | activemodel (~> 4.0)
227 | simplecov (0.10.0)
228 | docile (~> 1.1.0)
229 | json (~> 1.8)
230 | simplecov-html (~> 0.10.0)
231 | simplecov-html (0.10.0)
232 | slop (3.6.0)
233 | spring (1.4.0)
234 | spring-commands-rspec (1.0.4)
235 | spring (>= 0.9.1)
236 | sprockets (3.4.0)
237 | rack (> 1, < 3)
238 | sprockets-rails (2.3.3)
239 | actionpack (>= 3.0)
240 | activesupport (>= 3.0)
241 | sprockets (>= 2.8, < 4.0)
242 | term-ansicolor (1.3.2)
243 | tins (~> 1.0)
244 | terminal-table (1.5.2)
245 | thor (0.19.1)
246 | thread (0.2.2)
247 | thread_safe (0.3.5)
248 | tilt (2.0.1)
249 | timecop (0.8.0)
250 | tins (1.6.0)
251 | title (0.0.5)
252 | i18n
253 | rails (>= 3.1)
254 | tzinfo (1.2.2)
255 | thread_safe (~> 0.1)
256 | uglifier (2.7.2)
257 | execjs (>= 0.3.0)
258 | json (>= 1.8.0)
259 | web-console (2.2.1)
260 | activemodel (>= 4.0)
261 | binding_of_caller (>= 0.7.2)
262 | railties (>= 4.0)
263 | sprockets-rails (>= 2.0, < 4.0)
264 | webmock (1.22.1)
265 | addressable (>= 2.3.6)
266 | crack (>= 0.3.2)
267 | hashdiff
268 | xpath (2.0.0)
269 | nokogiri (~> 1.3)
270 |
271 | PLATFORMS
272 | ruby
273 |
274 | DEPENDENCIES
275 | airbrake
276 | autoprefixer-rails
277 | awesome_print
278 | bcrypt (~> 3.1.7)
279 | bourbon (~> 4.2.0)
280 | bundler-audit
281 | byebug
282 | capybara-webkit
283 | coffee-rails (~> 4.1.0)
284 | database_cleaner
285 | delayed_job_active_record
286 | dotenv-rails
287 | email_validator
288 | factory_girl_rails
289 | flutie
290 | formulaic
291 | friendly_id
292 | gravatar_image_tag (~> 1.2)
293 | high_voltage
294 | i18n-tasks
295 | jquery-rails
296 | kaminari
297 | launchy
298 | neat (~> 1.7.0)
299 | newrelic_rpm (>= 3.9.8)
300 | normalize-rails (~> 3.0.0)
301 | pg
302 | pry-rails
303 | puma
304 | quiet_assets
305 | rack-canonical-host
306 | rack-timeout
307 | rails (~> 4.2.0)
308 | recipient_interceptor
309 | refills
310 | rspec-rails (~> 3.3.0)
311 | sass-rails (~> 5.0)
312 | shoulda-matchers
313 | simple_form
314 | simplecov
315 | spring
316 | spring-commands-rspec
317 | timecop
318 | title
319 | uglifier
320 | web-console
321 | webmock
322 |
323 | BUNDLED WITH
324 | 1.10.6
325 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: bundle exec puma -p $PORT -C ./config/puma.rb
2 | worker: bundle exec rake jobs:work
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Ruby Bookshelf
2 |
3 | [](https://circleci.com/gh/taylorrf/rubybookshelf)
4 |
5 | ## Getting Started
6 |
7 | After you have cloned this repo, run this setup script to set up your machine
8 | with the necessary dependencies to run and test this app:
9 |
10 | % ./bin/setup
11 |
12 | It assumes you have a machine equipped with Ruby, Postgres, etc. If not, set up
13 | your machine with [this script].
14 |
15 | [this script]: https://github.com/thoughtbot/laptop
16 |
17 | After setting up, you can run the application using [foreman]:
18 |
19 | % foreman start
20 |
21 | If you don't have `foreman`, see [Foreman's install instructions][foreman]. It
22 | is [purposefully excluded from the project's `Gemfile`][exclude].
23 |
24 | [foreman]: https://github.com/ddollar/foreman
25 | [exclude]: https://github.com/ddollar/foreman/pull/437#issuecomment-41110407
26 |
27 | ## Useful Links
28 |
29 | * [GitHub repo](https://github.com/taylorrf/rubybookshelf)
30 | * [Production app on Heroku](http://rubybookshelf.herokuapp.com)
31 | * [Trello board](https://trello.com/b/rxNcTk1k/ruby-bookshelf)
32 | * [Slack room](https://theangledbrackets.slack.com/)
33 |
34 | ## Guidelines
35 |
36 | Use the following guides for getting things done, programming well, and
37 | programming in style.
38 |
39 | * [Protocol](http://github.com/thoughtbot/guides/blob/master/protocol)
40 | * [Best Practices](http://github.com/thoughtbot/guides/blob/master/best-practices)
41 | * [Style](http://github.com/thoughtbot/guides/blob/master/style)
42 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require File.expand_path('../config/application', __FILE__)
5 |
6 | Rails.application.load_tasks
7 | task(:default).clear
8 | task default: [:spec]
9 |
10 | if defined? RSpec
11 | task(:spec).clear
12 | RSpec::Core::RakeTask.new(:spec) do |t|
13 | t.verbose = false
14 | end
15 | end
16 |
17 | task default: "bundler:audit"
18 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "name":"rubybookshelf",
3 | "scripts":{
4 | "postdeploy":"bin/setup-pr-review"
5 | },
6 | "env":{
7 | "APPLICATION_HOST":{
8 | "required":true
9 | },
10 | "ASSET_HOST":{
11 | "required":true
12 | },
13 | "EXECJS_RUNTIME":{
14 | "required":true
15 | },
16 | "LANG":{
17 | "required":true
18 | },
19 | "RACK_ENV":{
20 | "required":true
21 | },
22 | "RAILS_ENV":{
23 | "required":true
24 | },
25 | "RAILS_SERVE_STATIC_FILES":{
26 | "required":true
27 | },
28 | "SECRET_KEY_BASE":{
29 | "required":true
30 | },
31 | "SMTP_ADDRESS":{
32 | "required":true
33 | },
34 | "SMTP_DOMAIN":{
35 | "required":true
36 | },
37 | "SMTP_PASSWORD":{
38 | "required":true
39 | },
40 | "SMTP_USERNAME":{
41 | "required":true
42 | }
43 | },
44 | "addons":[
45 | "heroku-postgresql"
46 | ]
47 | }
48 |
--------------------------------------------------------------------------------
/app/assets/images/default_cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taylorrf/rubybookshelf/8b94ee49cb6ebec4fcf567bb66298604c07dddfe/app/assets/images/default_cover.jpg
--------------------------------------------------------------------------------
/app/assets/images/logo_symbol.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taylorrf/rubybookshelf/8b94ee49cb6ebec4fcf567bb66298604c07dddfe/app/assets/images/logo_symbol.png
--------------------------------------------------------------------------------
/app/assets/javascripts/application.js:
--------------------------------------------------------------------------------
1 | // This is a manifest file that'll be compiled into application.js, which will include all the files
2 | // listed below.
3 | //
4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6 | //
7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8 | // compiled file.
9 | //
10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11 | // about supported directives.
12 | //
13 | //= require jquery
14 | //= require jquery_ujs
15 | //= require_tree .
16 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/application.scss:
--------------------------------------------------------------------------------
1 | @charset "utf-8";
2 |
3 | @import "normalize-rails";
4 | @import "bourbon";
5 | @import "base/grid-settings";
6 | @import "neat";
7 | @import "base/base";
8 | @import "refills/*";
9 | @import "layout/*";
10 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/base/_base.scss:
--------------------------------------------------------------------------------
1 | // Bitters 1.1.0
2 | // http://bitters.bourbon.io
3 | // Copyright 2013-2015 thoughtbot, inc.
4 | // MIT License
5 |
6 | @import "variables";
7 |
8 | // Neat Settings -- uncomment if using Neat -- must be imported before Neat
9 | // @import "grid-settings";
10 |
11 | @import "buttons";
12 | @import "forms";
13 | @import "lists";
14 | @import "tables";
15 | @import "typography";
16 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/base/_buttons.scss:
--------------------------------------------------------------------------------
1 | #{$all-buttons} {
2 | appearance: none;
3 | background-color: $action-color;
4 | border: 0;
5 | border-radius: $base-border-radius;
6 | color: #fff;
7 | cursor: pointer;
8 | display: inline-block;
9 | font-family: $base-font-family;
10 | font-size: $base-font-size;
11 | -webkit-font-smoothing: antialiased;
12 | font-weight: 600;
13 | line-height: 1;
14 | padding: $small-spacing $base-spacing;
15 | text-decoration: none;
16 | transition: background-color $base-duration $base-timing;
17 | user-select: none;
18 | vertical-align: middle;
19 | white-space: nowrap;
20 |
21 | &:hover,
22 | &:focus {
23 | background-color: shade($action-color, 20%);
24 | color: #fff;
25 | }
26 |
27 | &:disabled {
28 | cursor: not-allowed;
29 | opacity: 0.5;
30 |
31 | &:hover {
32 | background-color: $action-color;
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/base/_forms.scss:
--------------------------------------------------------------------------------
1 | fieldset {
2 | background-color: $secondary-background-color;
3 | border: $base-border;
4 | margin: 0 0 $small-spacing;
5 | padding: $base-spacing;
6 | }
7 |
8 | input,
9 | label,
10 | select {
11 | display: block;
12 | font-family: $base-font-family;
13 | font-size: $base-font-size;
14 | }
15 |
16 | label {
17 | font-weight: 600;
18 | margin-bottom: $small-spacing / 2;
19 |
20 | &.required::after {
21 | content: "*";
22 | }
23 |
24 | abbr {
25 | display: none;
26 | }
27 | }
28 |
29 | #{$all-text-inputs},
30 | select[multiple=multiple] {
31 | background-color: $base-background-color;
32 | border: $base-border;
33 | border-radius: $base-border-radius;
34 | box-shadow: $form-box-shadow;
35 | box-sizing: border-box;
36 | font-family: $base-font-family;
37 | font-size: $base-font-size;
38 | margin-bottom: $small-spacing;
39 | padding: $base-spacing / 3;
40 | transition: border-color $base-duration $base-timing;
41 | width: 100%;
42 |
43 | &:hover {
44 | border-color: shade($base-border-color, 20%);
45 | }
46 |
47 | &:focus {
48 | border-color: $action-color;
49 | box-shadow: $form-box-shadow-focus;
50 | outline: none;
51 | }
52 |
53 | &:disabled {
54 | background-color: shade($base-background-color, 5%);
55 | cursor: not-allowed;
56 |
57 | &:hover {
58 | border: $base-border;
59 | }
60 | }
61 | }
62 |
63 | textarea {
64 | resize: vertical;
65 | }
66 |
67 | input[type="search"] {
68 | appearance: none;
69 | }
70 |
71 | input[type="checkbox"],
72 | input[type="radio"] {
73 | display: inline;
74 | margin-right: $small-spacing / 2;
75 |
76 | + label {
77 | display: inline-block;
78 | }
79 | }
80 |
81 | input[type="file"] {
82 | margin-bottom: $small-spacing;
83 | width: 100%;
84 | }
85 |
86 | select {
87 | margin-bottom: $base-spacing;
88 | max-width: 100%;
89 | width: auto;
90 | }
91 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/base/_grid-settings.scss:
--------------------------------------------------------------------------------
1 | @import "neat-helpers"; // or "../neat/neat-helpers" when not in Rails
2 |
3 | // Neat Overrides
4 | // $column: 90px;
5 | // $gutter: 30px;
6 | // $grid-columns: 12;
7 | // $max-width: 1200px;
8 |
9 | // Neat Breakpoints
10 | $medium-screen: 600px;
11 | $large-screen: 900px;
12 |
13 | $medium-screen-up: new-breakpoint(min-width $medium-screen 4);
14 | $large-screen-up: new-breakpoint(min-width $large-screen 8);
15 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/base/_lists.scss:
--------------------------------------------------------------------------------
1 | ul,
2 | ol {
3 | list-style-type: none;
4 | margin: 0;
5 | padding: 0;
6 |
7 | &%default-ul {
8 | list-style-type: disc;
9 | margin-bottom: $small-spacing;
10 | padding-left: $base-spacing;
11 | }
12 |
13 | &%default-ol {
14 | list-style-type: decimal;
15 | margin-bottom: $small-spacing;
16 | padding-left: $base-spacing;
17 | }
18 | }
19 |
20 | dl {
21 | margin-bottom: $small-spacing;
22 |
23 | dt {
24 | font-weight: bold;
25 | margin-top: $small-spacing;
26 | }
27 |
28 | dd {
29 | margin: 0;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/base/_tables.scss:
--------------------------------------------------------------------------------
1 | table {
2 | border-collapse: collapse;
3 | font-feature-settings: "kern", "liga", "tnum";
4 | margin: $small-spacing 0;
5 | table-layout: fixed;
6 | width: 100%;
7 | }
8 |
9 | th {
10 | border-bottom: 1px solid shade($base-border-color, 25%);
11 | font-weight: 600;
12 | padding: $small-spacing 0;
13 | text-align: left;
14 | }
15 |
16 | td {
17 | border-bottom: $base-border;
18 | padding: $small-spacing 0;
19 | }
20 |
21 | tr,
22 | td,
23 | th {
24 | vertical-align: middle;
25 | }
26 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/base/_typography.scss:
--------------------------------------------------------------------------------
1 | body {
2 | color: $base-font-color;
3 | font-family: $base-font-family;
4 | font-feature-settings: "kern", "liga", "pnum";
5 | font-size: $base-font-size;
6 | line-height: $base-line-height;
7 | }
8 |
9 | h1,
10 | h2,
11 | h3,
12 | h4,
13 | h5,
14 | h6 {
15 | font-family: $heading-font-family;
16 | font-size: $base-font-size;
17 | line-height: $heading-line-height;
18 | margin: 0 0 $small-spacing;
19 | }
20 |
21 | p {
22 | margin: 0 0 $small-spacing;
23 | }
24 |
25 | a {
26 | color: $action-color;
27 | text-decoration: none;
28 | transition: color $base-duration $base-timing;
29 |
30 | &:active,
31 | &:focus,
32 | &:hover {
33 | color: shade($action-color, 25%);
34 | }
35 | }
36 |
37 | hr {
38 | border-bottom: $base-border;
39 | border-left: 0;
40 | border-right: 0;
41 | border-top: 0;
42 | margin: $base-spacing 0;
43 | }
44 |
45 | img,
46 | picture {
47 | margin: 0;
48 | max-width: 100%;
49 | }
50 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/base/_variables.scss:
--------------------------------------------------------------------------------
1 | // Typography
2 | $base-font-family: $helvetica;
3 | $heading-font-family: $base-font-family;
4 |
5 | $bookshelf-red: #E01520;
6 | $bookshelf-red-1: #EC1622;
7 | $bookshelf-red-2: #990E16;
8 |
9 | // Font Sizes
10 | $base-font-size: 1em;
11 | $font-size-smallest: rem(12);
12 | $font-size-small: 0.96rem;
13 |
14 | // Line height
15 | $base-line-height: 1.5;
16 | $heading-line-height: 1.2;
17 |
18 | // Other Sizes
19 | $base-border-radius: 3px;
20 | $base-spacing: $base-line-height * 1em;
21 | $small-spacing: $base-spacing / 2;
22 | $base-z-index: 0;
23 | $side-padding: 1em;
24 | $navigation-height: 60px;
25 |
26 | // Colors
27 | $blue: #477dca;
28 | $dark-gray: #333;
29 | $medium-gray: #999;
30 | $light-gray: #ddd;
31 | $white: #fff;
32 |
33 | // Font Colors
34 | $base-font-color: $dark-gray;
35 | $action-color: $blue;
36 |
37 | // Border
38 | $base-border-color: $light-gray;
39 | $base-border: 1px solid $base-border-color;
40 |
41 | // Background Colors
42 | $base-background-color: #fff;
43 | $secondary-background-color: tint($base-border-color, 75%);
44 |
45 | // Forms
46 | $form-box-shadow: inset 0 1px 3px rgba(#000, 0.06);
47 | $form-box-shadow-focus: $form-box-shadow, 0 0 5px adjust-color($action-color, $lightness: -5%, $alpha: -0.3);
48 |
49 | // Animations
50 | $base-duration: 150ms;
51 | $base-timing: ease;
52 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/layout/_booklist.scss:
--------------------------------------------------------------------------------
1 | .book {
2 | $base-border-radius: 3px !default;
3 | $base-spacing: 1.5em !default;
4 | $action-color: #477dca !default;
5 | $dark-gray: #333 !default;
6 | $base-font-color: $dark-gray !default;
7 | $book-gutter: 1.4em;
8 | $book-image-width: 150px;
9 | $book-color: $base-font-color;
10 | $book-image-vert-alignment: top;
11 |
12 | border-bottom: 1px solid transparentize($book-color, 0.9);
13 | display: table;
14 | margin-bottom: $base-spacing;
15 | padding-bottom: 1em;
16 | width: 100%;
17 |
18 | .book-image,
19 | .book-content {
20 | display: table-cell;
21 | vertical-align: $book-image-vert-alignment;
22 | }
23 |
24 | .book-image {
25 | padding-right: $book-gutter;
26 |
27 | > img {
28 | border-radius: $base-border-radius;
29 | display: block;
30 | height: auto;
31 | max-width: none;
32 | width: $book-image-width;
33 | }
34 |
35 | a {
36 | display: block;
37 | height: auto;
38 | max-width: none;
39 | width: $book-image-width;
40 | }
41 | }
42 |
43 | .book-content {
44 | width: 100%;
45 |
46 | a {
47 | color: $bookshelf-red-2;
48 | font-size: 1em;
49 | }
50 |
51 | a:hover {
52 | text-decoration: underline;
53 | }
54 |
55 | h1 {
56 | color: $bookshelf-red-2;
57 | font-size: 1.5em;
58 | margin: 0 0 0.5em;
59 | }
60 |
61 | h3 {
62 | color: $bookshelf-red-2;
63 | margin-bottom: 20px;
64 | }
65 |
66 | p {
67 | line-height: 1.5em;
68 | margin-bottom: 0.5em;
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/layout/_default_list.scss:
--------------------------------------------------------------------------------
1 | .authors-index,
2 | .lists-index {
3 | $base-border-radius: 3px !default;
4 | $base-spacing: 1em !default;
5 | $inline-books-image-width: 150px;
6 | $inline-books-color: $base-font-color;
7 |
8 | .authors,
9 | .lists {
10 | width: 100%;
11 |
12 | li {
13 | border-bottom: 1px solid transparentize($inline-books-color, 0.9);
14 | padding-bottom: 0.5em;
15 | }
16 | }
17 |
18 | ul.books {
19 | width: 100%;
20 |
21 | li {
22 | display: inline-flex;
23 | border-bottom: 0;
24 | margin: 0 10px;
25 | }
26 |
27 | > img {
28 | border-radius: $base-border-radius;
29 | height: auto;
30 | max-width: none;
31 | width: $inline-books-image-width;
32 | }
33 |
34 | a {
35 | height: auto;
36 | width: $inline-books-image-width;
37 | display: block;
38 | border-bottom: 3px solid $white;
39 | }
40 |
41 | a:hover {
42 | border-bottom: 3px solid $bookshelf-red-2;
43 | }
44 |
45 | }
46 |
47 | h2 {
48 | font-size: 1.4em;
49 |
50 | a {
51 | color: $bookshelf-red-2;
52 | }
53 |
54 | a:hover {
55 | text-decoration: underline;
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/layout/_layout.scss:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: $white;
3 | border-top: none;
4 | display: flex;
5 | flex-direction: column;
6 | font-family: $base-font-family;
7 | line-height: 20px;
8 | margin: 0 auto;
9 | min-height: 100vh;
10 | min-width: 320px;
11 | }
12 |
13 | .content {
14 | @include outer-container;
15 | max-width: $max-width;
16 | flex: 1;
17 | padding: 4em $side-padding 6em;
18 | width: 100%;
19 |
20 | h1 {
21 | font-size: 2.4em;
22 | }
23 |
24 | ul, ol {
25 | padding: 0;
26 | }
27 |
28 | li {
29 | margin-top: 1rem;
30 |
31 | &:first-child {
32 | margin-top: 0;
33 | }
34 | }
35 |
36 | .box {
37 | background: lighten($light-gray, 11%);
38 | border-radius: $base-border-radius;
39 | box-shadow: inset 0 0 1px $light-gray, 0 2px 4px darken($base-background-color, 10%);
40 | padding: 3em;
41 | }
42 |
43 | .author {
44 | p {
45 | margin-bottom: 40px;
46 | }
47 | }
48 |
49 | }
50 |
51 | .filters {
52 | align-items: center;
53 | display: flex;
54 | justify-content: left;
55 | padding-bottom: 30px;
56 |
57 | a {
58 | padding: 10px;
59 | }
60 |
61 | .new_filter {
62 | width: 20%;
63 |
64 | select {
65 | float: left;
66 | margin-right: 10px;
67 | }
68 | }
69 | }
70 |
71 | .registrations .box,
72 | .sessions .box {
73 | margin: 0 auto;
74 | width: 40%;
75 |
76 | input[type="submit"] {
77 | margin: 10px 0 20px;
78 | width: 100%;
79 | }
80 | }
81 |
82 | @mixin dashboard-large {
83 | @media screen and (min-width: 1050px) {
84 | @content;
85 | }
86 | }
87 |
88 | @mixin dashboard-medium {
89 | @media screen and (min-width: 800px) {
90 | @content;
91 | }
92 | }
93 |
94 | @mixin dashboard-small-only {
95 | @media screen and (max-width: 800px) {
96 | @content;
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/refills/_comments.scss:
--------------------------------------------------------------------------------
1 | .comment {
2 | $base-border-radius: 3px !default;
3 | $base-spacing: 1.5em !default;
4 | $action-color: $bookshelf-red-2 !default;
5 | $dark-gray: #333 !default;
6 | $base-font-color: $dark-gray !default;
7 | $comment-gutter: 1.4em;
8 | $comment-image-padding: 0.1em;
9 | $comment-image-width: 4em;
10 | $comment-color: $base-font-color;
11 | $comment-background: lighten($action-color, 15%);
12 | $comment-detail-color: transparentize($comment-color, 0.5);
13 | $comment-image-vert-alignment: top;
14 |
15 | border-bottom: 1px solid transparentize($comment-color, 0.9);
16 | display: table;
17 | margin-bottom: $base-spacing;
18 | padding-bottom: 1em;
19 | width: 100%;
20 |
21 | .comment-image,
22 | .comment-content {
23 | display: table-cell;
24 | vertical-align: $comment-image-vert-alignment;
25 | }
26 |
27 | .comment-image {
28 | padding-right: $comment-gutter;
29 |
30 | > img {
31 | border-radius: $base-border-radius;
32 | display: block;
33 | height: auto;
34 | max-width: none;
35 | padding: 0;
36 | width: $comment-image-width;
37 | }
38 |
39 | .comment-reverse-order & {
40 | padding-left: 10px;
41 | padding-right: 0;
42 | }
43 | }
44 |
45 | .comment-content {
46 | width: 100%;
47 |
48 | h1 {
49 | font-size: 1em;
50 | margin: 0 0 0.5em;
51 | }
52 |
53 | p {
54 | line-height: 1.5em;
55 | margin-bottom: 0.5em;
56 | }
57 |
58 | span.timestamp {
59 | color: $comment-detail-color;
60 | font-size: 0.8em;
61 | font-style: italic;
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/refills/_flashes.scss:
--------------------------------------------------------------------------------
1 | $base-spacing: 1.5em !default;
2 | $alert-color: #fff6bf !default;
3 | $error-color: #fbe3e4 !default;
4 | $notice-color: #e5edf8 !default;
5 | $success-color: #e6efc2 !default;
6 |
7 | @mixin flash($color) {
8 | background-color: $color;
9 | color: darken($color, 60%);
10 | display: block;
11 | font-weight: 600;
12 | margin-bottom: $base-spacing / 2;
13 | padding: $base-spacing / 2;
14 | text-align: center;
15 |
16 | a {
17 | color: darken($color, 70%);
18 |
19 | &:focus,
20 | &:hover {
21 | color: darken($color, 90%);
22 | }
23 | }
24 | }
25 |
26 | .flash-alert {
27 | @include flash($alert-color);
28 | }
29 |
30 | .flash-error {
31 | @include flash($error-color);
32 | }
33 |
34 | .flash-notice {
35 | @include flash($notice-color);
36 | }
37 |
38 | .flash-success {
39 | @include flash($success-color);
40 | }
41 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/refills/_footer.scss:
--------------------------------------------------------------------------------
1 | .footer {
2 | $base-spacing: 1.5em !default;
3 | $base-accent-color: $bookshelf-red-2 !default;
4 | $medium-screen: em(640) !default;
5 | $large-screen: em(860) !default;
6 |
7 | ul {
8 | padding: 0;
9 | }
10 |
11 | li {
12 | list-style: none;
13 | line-height: 1.5em;
14 | }
15 |
16 | a {
17 | text-decoration: none;
18 | }
19 |
20 | $footer-background: $base-accent-color;
21 | $footer-color: white;
22 | $footer-link-color: transparentize($footer-color, 0.4);
23 | $footer-disclaimer-color: transparentize($footer-color, 0.5);
24 |
25 | background: $footer-background;
26 | padding: ($base-spacing * 2) $gutter;
27 | width: 100%;
28 |
29 | .footer-logo {
30 | margin-bottom: 2em;
31 | text-align: center;
32 |
33 | img {
34 | height: 3em;
35 | }
36 | }
37 |
38 | .footer-links {
39 | @include clearfix;
40 | margin-bottom: $base-spacing;
41 |
42 | @include media($medium-screen) {
43 | @include shift(3);
44 | }
45 | }
46 |
47 | ul {
48 | margin-bottom: $base-spacing * 2;
49 |
50 | @include media($medium-screen) {
51 | @include span-columns(3);
52 | @include omega(3n);
53 | @include clearfix;
54 | }
55 | }
56 |
57 | li {
58 | text-align: center;
59 |
60 | @include media($medium-screen) {
61 | text-align: left;
62 | }
63 | }
64 |
65 | li a {
66 | color: $footer-link-color;
67 |
68 | &:focus,
69 | &:hover {
70 | color: transparentize($footer-color, 0);
71 | }
72 | }
73 |
74 | li h3 {
75 | color: $footer-color;
76 | font-size: 1em;
77 | font-weight: 800;
78 | margin-bottom: 0.4em;
79 | }
80 |
81 | hr {
82 | border: 1px solid transparentize($footer-disclaimer-color, 0.3);
83 | margin: 0 auto $base-spacing;
84 | width: 12em;
85 | }
86 |
87 | p {
88 | color: $footer-disclaimer-color;
89 | font-size: 0.9em;
90 | line-height: 1.5em;
91 | margin: auto;
92 | max-width: 35em;
93 | text-align: center;
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/refills/_header.scss:
--------------------------------------------------------------------------------
1 | .header-2 {
2 | $base-spacing: 1.5em !default;
3 | $base-accent-color: $bookshelf-red-2 !default;
4 | $medium-screen: em(640) !default;
5 | $large-screen: em(860) !default;
6 |
7 | ul {
8 | margin: 0;
9 | padding: 0;
10 | line-height: 1.5em;
11 | }
12 |
13 | li {
14 | list-style: none;
15 | }
16 |
17 | a {
18 | text-decoration: none;
19 | }
20 |
21 | $header-background: $base-accent-color;
22 | $header-color: white;
23 | $header-link-color: transparentize($header-color, 0.2);
24 | $header-disclaimer-color: transparentize($header-color, 0.6);
25 |
26 | background: $header-background;
27 | padding: $base-spacing;
28 | width: 100%;
29 | display: inline-block;
30 |
31 | .header-logo {
32 | margin-right: 1em;
33 | margin-bottom: 1em;
34 |
35 | @include media($large-screen) {
36 | float: left;
37 | margin-bottom: 0;
38 | }
39 | }
40 |
41 | .header-logo img {
42 | height: 1.6em;
43 | }
44 |
45 | ul {
46 | margin-bottom: 1em;
47 |
48 | @include media($large-screen) {
49 | float: left;
50 | line-height: 1.8em;
51 | margin-left: 1em;
52 | margin-bottom: 0;
53 | }
54 | }
55 |
56 | ul li {
57 | font-weight: 800;
58 | padding-right: 1em;
59 |
60 | @include media($large-screen) {
61 | display: inline;
62 | text-align: left;
63 | }
64 | }
65 |
66 | ul li a {
67 | color: $header-link-color;
68 |
69 | &:focus,
70 | &:hover {
71 | color: transparentize($header-color, 0);
72 | }
73 | }
74 |
75 | .header-secondary-links {
76 | @include media($large-screen) {
77 | float: right;
78 | }
79 |
80 | li {
81 | font-size: 0.8em;
82 | font-weight: 400;
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/refills/_hero.scss:
--------------------------------------------------------------------------------
1 | .hero {
2 | $base-border-radius: 3px !default;
3 | $base-accent-color: $white !default;
4 | $large-screen: em(860) !default;
5 |
6 | $hero-background-top: $bookshelf-red-1;
7 | $hero-background-bottom: $bookshelf-red-2;
8 | $hero-color: $white;
9 | $gradient-angle: 10deg;
10 | $hero-image: "https://raw.githubusercontent.com/thoughtbot/refills/master/source/images/mountains.png";
11 |
12 | @include background(url($hero-image), linear-gradient($gradient-angle, $hero-background-bottom, $hero-background-top), no-repeat $hero-background-top scroll);
13 | background-color: $bookshelf-red;
14 | background-position: top;
15 | background-repeat: no-repeat;
16 | background-size: cover;
17 | padding-bottom: 3em;
18 |
19 | .hero-logo img {
20 | height: 4em;
21 | margin-bottom: 1em;
22 | }
23 |
24 | .hero-inner {
25 | @include outer-container;
26 | @include clearfix;
27 | color: $hero-color;
28 | margin: auto;
29 | padding: 3.5em;
30 | text-align: center;
31 |
32 | .hero-copy {
33 | text-align: center;
34 |
35 | h1 {
36 | font-size: 1.6em;
37 | margin-bottom: 0.5em;
38 |
39 | @include media($large-screen) {
40 | font-size: 1.8em;
41 | }
42 | }
43 |
44 | p {
45 | font-weight: 200;
46 | line-height: 1.4em;
47 | margin: 0 auto 3em;
48 |
49 | @include media($large-screen) {
50 | font-size: 1.1em;
51 | max-width: 40%;
52 | }
53 | }
54 | }
55 |
56 | .button {
57 | @include button(flat, $base-accent-color);
58 | padding: 0.7em 1em;
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | include Authentication
3 | protect_from_forgery with: :exception
4 | end
5 |
--------------------------------------------------------------------------------
/app/controllers/authors_controller.rb:
--------------------------------------------------------------------------------
1 | class AuthorsController < ApplicationController
2 | def index
3 | render locals: { authors: Author.all }
4 | end
5 |
6 | def show
7 | author = Author.find(params[:id])
8 | render locals: { author: author }
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/app/controllers/books/filter_controller.rb:
--------------------------------------------------------------------------------
1 | module Books
2 | class FilterController < ApplicationController
3 | def show
4 | filter = Filter.new(params[:filter])
5 | render "books/index", locals: {
6 | books: filter.matches.page(params[:page]),
7 | filter: filter
8 | }
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/controllers/books_controller.rb:
--------------------------------------------------------------------------------
1 | class BooksController < ApplicationController
2 | def index
3 | render locals: {
4 | books: Book.sort_by(params[:sort]).page(params[:page]),
5 | filter: Filter.new
6 | }
7 | end
8 |
9 | def show
10 | facade = BookFacade.new(book: book, current_user: current_user)
11 | render locals: { facade: facade }
12 | end
13 |
14 | private
15 |
16 | def book
17 | @_book ||= Book.find params[:id]
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/app/controllers/concerns/authentication.rb:
--------------------------------------------------------------------------------
1 | module Authentication
2 | extend ActiveSupport::Concern
3 |
4 | included do
5 | helper_method :authorize, :current_user, :signed_in?
6 | end
7 |
8 | def authorize
9 | redirect_to login_path unless current_user
10 | end
11 |
12 | def current_user
13 | @_current_user ||= User.find_by(id: session[:user_id]) || Guest.new
14 | end
15 |
16 | def signed_in?
17 | current_user.present?
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/app/controllers/lists_controller.rb:
--------------------------------------------------------------------------------
1 | class ListsController < ApplicationController
2 | def index
3 | render locals: { lists: List.newest_first }
4 | end
5 |
6 | def show
7 | render locals: { list: List.find(params[:id]) }
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/controllers/pages_controller.rb:
--------------------------------------------------------------------------------
1 | class PagesController < ApplicationController
2 | def home
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/app/controllers/registrations_controller.rb:
--------------------------------------------------------------------------------
1 | class RegistrationsController < ApplicationController
2 | def new
3 | render :new, locals: { registration: User.new }
4 | end
5 |
6 | def create
7 | user = User.new user_params
8 |
9 | if user.save
10 | session.update user_id: user.id
11 | redirect_to root_url, notice: t("registration.success", email: user.email)
12 | else
13 | render :new, locals: { registration: user }
14 | end
15 | end
16 |
17 | private
18 |
19 | def user_params
20 | params.require(:registration).permit(:email, :password)
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/app/controllers/reviews_controller.rb:
--------------------------------------------------------------------------------
1 | class ReviewsController < ApplicationController
2 | before_action :authorize
3 |
4 | def create
5 | review = Review.new review_params
6 |
7 | if review.save
8 | redirect_to book_path(book), notice: t("reviews.create.notice")
9 | else
10 | render template: "books/show", locals: { facade: book_facade }
11 | end
12 | end
13 |
14 | private
15 |
16 | def review_params
17 | params.
18 | require(:review).
19 | permit(:body, :rating).
20 | merge(reviewer: current_user, book: book)
21 | end
22 |
23 | def book
24 | @_book ||= Book.find params[:book_id]
25 | end
26 |
27 | def book_facade
28 | BookFacade.new(book: book, current_user: current_user)
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/app/controllers/sessions_controller.rb:
--------------------------------------------------------------------------------
1 | class SessionsController < ApplicationController
2 | def new
3 | end
4 |
5 | def create
6 | user = User.lookup_for_authentication_with identifier: email
7 |
8 | if user.authenticate password
9 | session.update user_id: user.id
10 | redirect_to root_url, notice: t("session.success", email: user.email)
11 | else
12 | flash.now[:error] = t("session.error")
13 | render :new
14 | end
15 | end
16 |
17 | def destroy
18 | session.clear
19 |
20 | redirect_to root_url
21 | end
22 |
23 | private
24 |
25 | def session_params
26 | params.require(:session).permit(:email, :password)
27 | end
28 |
29 | def email
30 | session_params.fetch :email
31 | end
32 |
33 | def password
34 | session_params.fetch :password
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/app/facades/book_facade.rb:
--------------------------------------------------------------------------------
1 | class BookFacade
2 | def initialize(book:, current_user:)
3 | @book = book
4 | @current_user = current_user
5 | end
6 |
7 | attr_reader :book
8 |
9 | def review_policy
10 | ReviewPolicy.new(
11 | reviewers: book_reviewers,
12 | reviewer_to_auth: current_user
13 | )
14 | end
15 |
16 | def new_review
17 | Review.new
18 | end
19 |
20 | private
21 |
22 | attr_reader :current_user
23 |
24 | def book_reviewers
25 | book.reviews.map(&:reviewer)
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/app/forms/filter.rb:
--------------------------------------------------------------------------------
1 | class Filter
2 | include ActiveModel::Model
3 |
4 | attr_accessor :genre_id
5 |
6 | def matches
7 | Book.where(genre_id: genre_id)
8 | end
9 |
10 | def genres
11 | Genre.containing_books
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | def logged_out?
3 | !current_user.present?
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/app/helpers/books_helper.rb:
--------------------------------------------------------------------------------
1 | module BooksHelper
2 | # can't figure why `url_for` works but `path_for` doesn't
3 | def sort_path(criteria = nil)
4 | url_for(
5 | controller: params[:controller],
6 | action: params[:action],
7 | sort: criteria,
8 | filter: params[:filter],
9 | page: params[:page]
10 | )
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/app/helpers/flashes_helper.rb:
--------------------------------------------------------------------------------
1 | module FlashesHelper
2 | def user_facing_flashes
3 | flash.to_hash.slice("alert", "error", "notice", "success")
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/app/mailers/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taylorrf/rubybookshelf/8b94ee49cb6ebec4fcf567bb66298604c07dddfe/app/mailers/.keep
--------------------------------------------------------------------------------
/app/models/author.rb:
--------------------------------------------------------------------------------
1 | class Author < ActiveRecord::Base
2 | extend FriendlyId
3 | friendly_id :name, use: :slugged
4 |
5 | validates :name, presence: true
6 | has_many :books
7 |
8 | def popular_books( limit=5 )
9 | books.limit(limit)
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/models/book.rb:
--------------------------------------------------------------------------------
1 | class Book < ActiveRecord::Base
2 | CannotCalculateAverageRatingError = Class.new(RuntimeError)
3 |
4 | extend FriendlyId
5 | friendly_id :title, use: :slugged
6 |
7 | SORT_CRITERIA_RELEASE_DATE = "release_date"
8 |
9 | belongs_to :genre
10 | belongs_to :author
11 | has_many :reviews
12 |
13 | def has_summary?
14 | summary.present?
15 | end
16 |
17 | def has_release_date?
18 | released_on.present?
19 | end
20 |
21 | def sample_cover
22 | (cover || "default_cover.jpg")
23 | end
24 |
25 | def self.sort_by(criteria)
26 | if criteria == SORT_CRITERIA_RELEASE_DATE
27 | newest_first
28 | else
29 | alphabetically
30 | end
31 | end
32 |
33 | def self.newest_first
34 | order(released_on: :desc)
35 | end
36 |
37 | def self.alphabetically
38 | order("lower(title)")
39 | end
40 |
41 | # TODO: extract this so as to minimize behaviour within the AR model
42 | def average_rating
43 | if reviews.any?
44 | reviews.with_ratings.average(:rating)
45 | else
46 | fail CannotCalculateAverageRatingError
47 | end
48 | end
49 |
50 | def has_average_rating?
51 | reviews.with_ratings.any?
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/app/models/concerns/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taylorrf/rubybookshelf/8b94ee49cb6ebec4fcf567bb66298604c07dddfe/app/models/concerns/.keep
--------------------------------------------------------------------------------
/app/models/genre.rb:
--------------------------------------------------------------------------------
1 | class Genre < ActiveRecord::Base
2 | has_many :books
3 |
4 | # TODO: sort alphabetically?
5 |
6 | def self.containing_books
7 | joins(:books).distinct
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/models/guest.rb:
--------------------------------------------------------------------------------
1 | class Guest
2 | def authenticate(_)
3 | false
4 | end
5 |
6 | def present?
7 | false
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/models/list.rb:
--------------------------------------------------------------------------------
1 | class List < ActiveRecord::Base
2 | extend FriendlyId
3 | friendly_id :name, use: :slugged
4 |
5 | has_many :list_entries
6 | has_many :books, through: :list_entries
7 |
8 | delegate :empty?, to: :books
9 |
10 | def self.newest_first
11 | order(created_at: :desc)
12 | end
13 |
14 | def highlights(maximum:)
15 | books.order("list_entries.created_at DESC").limit(maximum)
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/app/models/list_entry.rb:
--------------------------------------------------------------------------------
1 | class ListEntry < ActiveRecord::Base
2 | belongs_to :list
3 | belongs_to :book
4 | end
5 |
--------------------------------------------------------------------------------
/app/models/review.rb:
--------------------------------------------------------------------------------
1 | class Review < ActiveRecord::Base
2 | belongs_to :book
3 | belongs_to :reviewer, class_name: "User", foreign_key: :user_id
4 |
5 | validates :body, presence: true
6 |
7 | delegate :email, to: :reviewer, prefix: true
8 |
9 | def self.with_ratings
10 | where.not(rating: nil)
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/app/models/user.rb:
--------------------------------------------------------------------------------
1 | class User < ActiveRecord::Base
2 | has_secure_password
3 |
4 | validates :email, email: true, uniqueness: true
5 |
6 | def self.lookup_for_authentication_with(identifier:)
7 | find_by(email: identifier) || Guest.new
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/policies/review_policy.rb:
--------------------------------------------------------------------------------
1 | class ReviewPolicy
2 | attr_reader :reviewers, :reviewer_to_auth
3 |
4 | def initialize(reviewers:, reviewer_to_auth:)
5 | @reviewers = reviewers
6 | @reviewer_to_auth = reviewer_to_auth
7 | end
8 |
9 | def authorized_for_review?
10 | reviewer_is_logged_in? && has_not_already_reviewed?
11 | end
12 |
13 | private
14 |
15 | def reviewer_is_logged_in?
16 | reviewer_to_auth.present?
17 | end
18 |
19 | def has_not_already_reviewed?
20 | !reviewers.include? reviewer_to_auth
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/app/views/application/_analytics.html.erb:
--------------------------------------------------------------------------------
1 | <% if ENV["SEGMENT_KEY"] %>
2 |
7 | <% end %>
8 |
--------------------------------------------------------------------------------
/app/views/application/_flashes.html.erb:
--------------------------------------------------------------------------------
1 | <% if flash.any? %>
2 |
3 | <% user_facing_flashes.each do |key, value| -%>
4 |
<%= value %>
5 | <% end -%>
6 |
7 | <% end %>
8 |
--------------------------------------------------------------------------------
/app/views/application/_footer.html.erb:
--------------------------------------------------------------------------------
1 |
27 |
--------------------------------------------------------------------------------
/app/views/application/_header.html.erb:
--------------------------------------------------------------------------------
1 |
28 |
--------------------------------------------------------------------------------
/app/views/application/_javascript.html.erb:
--------------------------------------------------------------------------------
1 | <%= javascript_include_tag :application %>
2 |
3 | <%= yield :javascript %>
4 |
5 | <%= render "analytics" %>
6 |
7 | <% if Rails.env.test? %>
8 | <%= javascript_tag do %>
9 | $.fx.off = true;
10 | $.ajaxSetup({ async: false });
11 | <% end %>
12 | <% end %>
13 |
--------------------------------------------------------------------------------
/app/views/authors/index.html.erb:
--------------------------------------------------------------------------------
1 | <%= t(".header")%>
2 |
3 | <% if authors.any? %>
4 |
5 | <% authors.each do |author| %>
6 | -
7 |
<%= link_to author.name, author %>
8 |
9 | <% author.popular_books.each do |book| %>
10 | - <%= link_to image_tag(book.sample_cover), book %>
11 | <% end %>
12 |
13 |
14 | <% end %>
15 |
16 | <% else %>
17 | <%= t(".no_authors_found") %>
18 | <% end%>
19 |
--------------------------------------------------------------------------------
/app/views/authors/show.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
<%= author.name %>
3 |
<%= author.description %>
4 | <%= render "books/books", books: author.books %>
5 |
6 |
--------------------------------------------------------------------------------
/app/views/books/_books.html.erb:
--------------------------------------------------------------------------------
1 | <% books.each do |book| %>
2 |
3 |
4 | <%= link_to image_tag(book.sample_cover), book %>
5 |
6 |
7 |
<%= link_to book.title, book %>
8 |
<%= book.summary %>
9 |
10 |
11 | <% end %>
12 |
--------------------------------------------------------------------------------
/app/views/books/_details.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= image_tag(book.sample_cover) %>
4 |
5 |
6 |
<%= book.title %>
7 | <% if book.author.present? %>
8 |
by <%= link_to book.author.name, book.author %>
9 | <% end %>
10 |
11 | <% if book.has_summary? %>
12 | <% # doesn't yet handle paragraphs, line breaks yet %>
13 | <%= simple_format book.summary, class: "summary" %>
14 | <% end %>
15 |
16 | <% if book.has_release_date? %>
17 |
<%= l(book.released_on, format: :long) %>
18 | <% end %>
19 |
20 | <% if book.has_average_rating? %>
21 |
Average rating:
22 | <%= book.average_rating %>
23 |
24 | <% end %>
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/views/books/_filter.html.erb:
--------------------------------------------------------------------------------
1 | <%= simple_form_for filter, url: book_filter_path, method: :get do |form| %>
2 | <%= form.input :genre_id, collection: filter.genres, include_blank: true %>
3 | <%= form.submit t(".button") %>
4 | <% end %>
5 |
--------------------------------------------------------------------------------
/app/views/books/_reviews.html.erb:
--------------------------------------------------------------------------------
1 |
4 | <% if review_policy.authorized_for_review? %>
5 | <%= render partial: "reviews/form", locals: { book: book, review: review } %>
6 | <% elsif logged_out? %>
7 | <%= link_to t("books.reviews.create_account"), new_registration_path %>
8 | <% end %>
9 | <%= render book.reviews %>
10 |
--------------------------------------------------------------------------------
/app/views/books/index.html.erb:
--------------------------------------------------------------------------------
1 | <%= t(".header")%>
2 |
3 | <% if books.any? %>
4 |
5 | <%= render "books/filter", filter: filter %>
6 | <%= link_to(t("sorting.by_title"), sort_path("title")) %> |
7 | <%= link_to(t("sorting.newest_first"), sort_path(Book::SORT_CRITERIA_RELEASE_DATE)) %>
8 |
9 |
10 | <%= render "books/books", books: books %>
11 |
12 | <%= paginate books %>
13 | <% else %>
14 | <%= t(".no_books_yet") %>
15 | <% end %>
16 |
--------------------------------------------------------------------------------
/app/views/books/show.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= render "details", book: facade.book %>
3 | <%= render "reviews", book: facade.book, review_policy: facade.review_policy, review: facade.new_review %>
4 |
5 |
--------------------------------------------------------------------------------
/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <%#
8 | Configure default and controller-, and view-specific titles in
9 | config/locales/en.yml. For more see:
10 | https://github.com/calebthompson/title#usage
11 | %>
12 | <%= title %>
13 | <%= stylesheet_link_tag :application, media: "all" %>
14 | <%= csrf_meta_tags %>
15 |
16 |
17 | <%= render "header"%>
18 | <%= render "flashes" -%>
19 |
20 |
21 | <%= yield %>
22 |
23 |
24 | <%= render "footer"%>
25 | <%= render "javascript" %>
26 |
27 |
28 |
--------------------------------------------------------------------------------
/app/views/lists/_list.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= link_to list.name, list %>
3 | <% if list.empty? %>
4 | <%= t(".empty") %>
5 | <% else %>
6 |
7 | <% list.highlights(maximum: 5).each do |book| %>
8 | - <%= link_to image_tag(book.sample_cover), book %>
9 | <% end %>
10 |
11 | <% end %>
12 |
13 |
--------------------------------------------------------------------------------
/app/views/lists/index.html.erb:
--------------------------------------------------------------------------------
1 | <%= t(".header")%>
2 |
3 | <% if lists.any? %>
4 |
5 | <%= render lists %>
6 |
7 | <% else %>
8 | <%= t(".no_lists_yet") %>
9 | <% end %>
10 |
--------------------------------------------------------------------------------
/app/views/lists/show.html.erb:
--------------------------------------------------------------------------------
1 | <%= list.name %>
2 |
3 | <% if list.empty? %>
4 | <%= t(".empty") %>
5 | <% else %>
6 | <%= render "books/books", books: list.books %>
7 | <% end %>
8 |
--------------------------------------------------------------------------------
/app/views/pages/home.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= image_tag "logo_symbol.png"%>
4 |
5 |
Curated Bookshelf for Rubyist
6 |
From the expert to the novice, find the best Ruby books and start to sharpen your skills.
7 |
8 | <%= link_to "Start Exploring", lists_path, class: "button"%>
9 |
10 |
11 |
--------------------------------------------------------------------------------
/app/views/registrations/new.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= simple_form_for registration, as: :registration, url: registration_path do |form_builder| %>
3 | <%= render partial: "shared/registration_form_fields",
4 | locals: { form_builder: form_builder } %>
5 |
6 | <%= form_builder.submit t("registration.register"),
7 | disable_with: t("registration.register_disable") %>
8 | <% end %>
9 |
10 | <%= link_to t("registration.already_have_account"), login_path %>
11 |
12 |
--------------------------------------------------------------------------------
/app/views/reviews/_form.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= simple_form_for [book, review] do |form_builder| %>
3 | <%= form_builder.input :body, label: t("reviews.form.body"), required: true %>
4 | <%= form_builder.input :rating, label: t("reviews.form.rating"), as: :select, collection: 1..5 %>
5 | <%= form_builder.submit t("reviews.form.submit"), disabled_with: t("reviews.form.submiting") %>
6 | <% end %>
7 |
8 |
--------------------------------------------------------------------------------
/app/views/reviews/_review.html.erb:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/app/views/sessions/new.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= simple_form_for :session, url: session_path do |form_builder| %>
3 | <%= render partial: "shared/registration_form_fields",
4 | locals: { form_builder: form_builder } %>
5 |
6 | <%= form_builder.submit t("registration.login"),
7 | disable_with: t("registration.login_disable") %>
8 | <% end %>
9 |
10 |
--------------------------------------------------------------------------------
/app/views/shared/_registration_form_fields.html.erb:
--------------------------------------------------------------------------------
1 | <%= form_builder.input :email, label: t("registration.email"), required: true %>
2 | <%= form_builder.input :password, label: t("registration.password"), required: true %>
3 |
--------------------------------------------------------------------------------
/bin/bundle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3 | load Gem.bin_path('bundler', 'bundle')
4 |
--------------------------------------------------------------------------------
/bin/delayed_job:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'config', 'environment'))
4 | require 'delayed/command'
5 | Delayed::Command.new(ARGV).daemonize
6 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | begin
3 | load File.expand_path("../spring", __FILE__)
4 | rescue LoadError
5 | end
6 | APP_PATH = File.expand_path('../../config/application', __FILE__)
7 | require_relative '../config/boot'
8 | require 'rails/commands'
9 |
--------------------------------------------------------------------------------
/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | begin
3 | load File.expand_path("../spring", __FILE__)
4 | rescue LoadError
5 | end
6 | require_relative '../config/boot'
7 | require 'rake'
8 | Rake.application.run
9 |
--------------------------------------------------------------------------------
/bin/rspec:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | begin
3 | load File.expand_path("../spring", __FILE__)
4 | rescue LoadError
5 | end
6 | require 'bundler/setup'
7 | load Gem.bin_path('rspec-core', 'rspec')
8 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | # Set up Rails app. Run this script immediately after cloning the codebase.
4 | # https://github.com/thoughtbot/guides/tree/master/protocol
5 |
6 | # Exit if any subcommand fails
7 | set -e
8 |
9 | # Set up Ruby dependencies via Bundler
10 | gem install bundler --conservative
11 | bundle check || bundle install
12 |
13 | # Set up configurable environment variables
14 | if [ ! -f .env ]; then
15 | cp .sample.env .env
16 | fi
17 |
18 | # Set up database and add any development seed data
19 | bundle exec rake db:setup dev:prime
20 |
21 | # Add binstubs to PATH via export PATH=".git/safe/../../bin:$PATH" in ~/.zshenv
22 | mkdir -p .git/safe
23 |
24 | # Pick a port for Foreman
25 | if ! grep --quiet --no-messages --fixed-strings 'port' .foreman; then
26 | printf 'port: 3000\n' >> .foreman
27 | fi
28 |
29 | if ! command -v foreman > /dev/null; then
30 | gem install foreman
31 | fi
32 |
33 | # Only if this isn't CI
34 | # if [ -z "$CI" ]; then
35 | # fi
36 |
--------------------------------------------------------------------------------
/bin/setup-pr-review:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | # Set up database for heroku pr review app
4 | bundle exec rake db:setup
5 |
--------------------------------------------------------------------------------
/bin/spring:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | # This file loads spring without using Bundler, in order to be fast.
4 | # It gets overwritten when you run the `spring binstub` command.
5 |
6 | unless defined?(Spring)
7 | require "rubygems"
8 | require "bundler"
9 |
10 | if (match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m))
11 | Gem.paths = { "GEM_PATH" => [Bundler.bundle_path.to_s, *Gem.path].uniq }
12 | gem "spring", match[1]
13 | require "spring/binstub"
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/browserslist:
--------------------------------------------------------------------------------
1 | Last 2 versions
2 | Explorer >= 9
3 | iOS >= 7.1
4 | Android >= 4.4
5 |
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | checkout:
2 | post:
3 | - cp .sample.env .env
4 | database:
5 | override:
6 | - bin/setup
7 | test:
8 | override:
9 | - bundle exec rake
10 | deployment:
11 | staging:
12 | branch: master
13 | heroku:
14 | appname: rubybookshelf-staging
15 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require ::File.expand_path('../config/environment', __FILE__)
4 | run Rails.application
5 |
--------------------------------------------------------------------------------
/config/application.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../boot', __FILE__)
2 | require "rails"
3 | require "active_model/railtie"
4 | require "active_job/railtie"
5 | require "active_record/railtie"
6 | require "action_controller/railtie"
7 | require "action_mailer/railtie"
8 | require "action_view/railtie"
9 | require "sprockets/railtie"
10 | Bundler.require(*Rails.groups)
11 | module Rubybookshelf
12 | class Application < Rails::Application
13 | config.i18n.enforce_available_locales = true
14 | config.quiet_assets = true
15 | config.generators do |generate|
16 | generate.helper false
17 | generate.javascript_engine false
18 | generate.request_specs false
19 | generate.routing_specs false
20 | generate.stylesheets false
21 | generate.test_framework :rspec
22 | generate.view_specs false
23 | end
24 | config.action_controller.action_on_unpermitted_parameters = :raise
25 | config.active_record.raise_in_transactional_callbacks = true
26 | config.active_job.queue_adapter = :delayed_job
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/config/boot.rb:
--------------------------------------------------------------------------------
1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
2 |
3 | require 'bundler/setup' # Set up gems listed in the Gemfile.
4 |
--------------------------------------------------------------------------------
/config/database.yml:
--------------------------------------------------------------------------------
1 | development: &default
2 | adapter: postgresql
3 | database: rubybookshelf_development
4 | encoding: utf8
5 | host: localhost
6 | min_messages: warning
7 | pool: <%= Integer(ENV.fetch("DB_POOL", 5)) %>
8 | reaping_frequency: <%= Integer(ENV.fetch("DB_REAPING_FREQUENCY", 10)) %>
9 | timeout: 5000
10 |
11 | test:
12 | <<: *default
13 | database: rubybookshelf_test
14 |
15 | production: &deploy
16 | encoding: utf8
17 | min_messages: warning
18 | pool: <%= [Integer(ENV.fetch("MAX_THREADS", 5)), Integer(ENV.fetch("DB_POOL", 5))].max %>
19 | timeout: 5000
20 | url: <%= ENV.fetch("DATABASE_URL", "") %>
21 |
22 | staging: *deploy
23 |
--------------------------------------------------------------------------------
/config/environment.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../application', __FILE__)
2 | Rails.application.initialize!
3 |
--------------------------------------------------------------------------------
/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | config.cache_classes = false
3 | config.eager_load = false
4 | config.consider_all_requests_local = true
5 | config.action_controller.perform_caching = false
6 | config.action_mailer.raise_delivery_errors = true
7 | config.action_mailer.delivery_method = :test
8 | config.active_support.deprecation = :log
9 | config.active_record.migration_error = :page_load
10 | config.assets.debug = true
11 | config.assets.digest = true
12 | config.assets.raise_runtime_errors = true
13 | config.action_view.raise_on_missing_translations = true
14 | config.action_mailer.default_url_options = { host: "localhost:3000" }
15 | end
16 |
--------------------------------------------------------------------------------
/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | require Rails.root.join("config/smtp")
2 | Rails.application.configure do
3 | config.cache_classes = true
4 | config.eager_load = true
5 | config.consider_all_requests_local = false
6 | config.action_controller.perform_caching = true
7 | config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present?
8 | config.middleware.use Rack::Deflater
9 | config.middleware.use Rack::CanonicalHost, ENV.fetch("APPLICATION_HOST")
10 | config.assets.js_compressor = :uglifier
11 | config.assets.compile = false
12 | config.assets.digest = true
13 | config.log_level = :debug
14 | config.action_controller.asset_host = ENV.fetch("ASSET_HOST", ENV.fetch("APPLICATION_HOST"))
15 | config.action_mailer.delivery_method = :smtp
16 | config.action_mailer.smtp_settings = SMTP_SETTINGS
17 | config.i18n.fallbacks = true
18 | config.active_support.deprecation = :notify
19 | config.log_formatter = ::Logger::Formatter.new
20 | config.active_record.dump_schema_after_migration = false
21 | config.action_mailer.default_url_options = { host: ENV.fetch("APPLICATION_HOST") }
22 | end
23 | Rack::Timeout.timeout = (ENV["RACK_TIMEOUT"] || 10).to_i
24 |
--------------------------------------------------------------------------------
/config/environments/staging.rb:
--------------------------------------------------------------------------------
1 | require_relative "production"
2 |
3 | Mail.register_interceptor(
4 | RecipientInterceptor.new(ENV.fetch("EMAIL_RECIPIENTS"))
5 | )
6 |
7 | Rails.application.configure do
8 | # ...
9 |
10 | config.action_mailer.default_url_options = { host: ENV.fetch("APPLICATION_HOST") }
11 | end
12 |
--------------------------------------------------------------------------------
/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | config.cache_classes = true
3 | config.eager_load = false
4 | config.serve_static_files = true
5 | config.static_cache_control = 'public, max-age=3600'
6 | config.consider_all_requests_local = true
7 | config.action_controller.perform_caching = false
8 | config.action_dispatch.show_exceptions = false
9 | config.action_controller.allow_forgery_protection = false
10 | config.action_mailer.delivery_method = :test
11 | config.active_support.test_order = :random
12 | config.active_support.deprecation = :stderr
13 | config.action_view.raise_on_missing_translations = true
14 | config.action_mailer.default_url_options = { host: "www.example.com" }
15 | config.active_job.queue_adapter = :inline
16 | end
17 |
--------------------------------------------------------------------------------
/config/i18n-tasks.yml:
--------------------------------------------------------------------------------
1 | search:
2 | paths:
3 | - "app/controllers"
4 | - "app/helpers"
5 | - "app/presenters"
6 | - "app/views"
7 |
8 | ignore_unused:
9 | - activerecord.*
10 | - date.*
11 | - simple_form.*
12 | - time.*
13 | - titles.*
14 |
--------------------------------------------------------------------------------
/config/initializers/assets.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Version of your assets, change this if you want to expire all your assets.
4 | Rails.application.config.assets.version = (ENV["ASSETS_VERSION"] || "1.0")
5 |
6 | # Add additional assets to the asset load path
7 | # Rails.application.config.assets.paths << Emoji.images_path
8 |
9 | # Precompile additional assets.
10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
11 | # Rails.application.config.assets.precompile += %w( search.js )
12 |
--------------------------------------------------------------------------------
/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 | # Rails.backtrace_cleaner.remove_silencers!
8 |
--------------------------------------------------------------------------------
/config/initializers/cookies_serializer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | Rails.application.config.action_dispatch.cookies_serializer = :json
4 |
--------------------------------------------------------------------------------
/config/initializers/disable_xml_params.rb:
--------------------------------------------------------------------------------
1 | ActionDispatch::ParamsParser::DEFAULT_PARSERS.delete(Mime::XML)
2 |
--------------------------------------------------------------------------------
/config/initializers/errors.rb:
--------------------------------------------------------------------------------
1 | require "net/http"
2 | require "net/smtp"
3 |
4 | # Example:
5 | # begin
6 | # some http call
7 | # rescue *HTTP_ERRORS => error
8 | # notify_hoptoad error
9 | # end
10 |
11 | HTTP_ERRORS = [
12 | EOFError,
13 | Errno::ECONNRESET,
14 | Errno::EINVAL,
15 | Net::HTTPBadResponse,
16 | Net::HTTPHeaderSyntaxError,
17 | Net::ProtocolError,
18 | Timeout::Error
19 | ]
20 |
21 | SMTP_SERVER_ERRORS = [
22 | IOError,
23 | Net::SMTPAuthenticationError,
24 | Net::SMTPServerBusy,
25 | Net::SMTPUnknownError,
26 | TimeoutError
27 | ]
28 |
29 | SMTP_CLIENT_ERRORS = [
30 | Net::SMTPFatalError,
31 | Net::SMTPSyntaxError
32 | ]
33 |
34 | SMTP_ERRORS = SMTP_SERVER_ERRORS + SMTP_CLIENT_ERRORS
35 |
--------------------------------------------------------------------------------
/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Configure sensitive parameters which will be filtered from the log file.
4 | Rails.application.config.filter_parameters += [:password]
5 |
--------------------------------------------------------------------------------
/config/initializers/friendly_id.rb:
--------------------------------------------------------------------------------
1 | FriendlyId.defaults do |config|
2 | config.use :reserved
3 |
4 | config.reserved_words = %w(
5 | new
6 | edit
7 | index
8 | session
9 | login
10 | logout
11 | users
12 | admin
13 | stylesheets
14 | assets
15 | javascripts
16 | images
17 | )
18 |
19 | config.use :finders
20 | end
21 |
--------------------------------------------------------------------------------
/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | # inflect.plural /^(ox)$/i, '\1en'
8 | # inflect.singular /^(ox)en/i, '\1'
9 | # inflect.irregular 'person', 'people'
10 | # inflect.uncountable %w( fish sheep )
11 | # end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | # inflect.acronym 'RESTful'
16 | # end
17 |
--------------------------------------------------------------------------------
/config/initializers/json_encoding.rb:
--------------------------------------------------------------------------------
1 | ActiveSupport::JSON::Encoding.time_precision = 0
2 |
--------------------------------------------------------------------------------
/config/initializers/kaminari_config.rb:
--------------------------------------------------------------------------------
1 | Kaminari.configure do |config|
2 | config.default_per_page = 20
3 | end
4 |
--------------------------------------------------------------------------------
/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new mime types for use in respond_to blocks:
4 | # Mime::Type.register "text/richtext", :rtf
5 |
--------------------------------------------------------------------------------
/config/initializers/session_store.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | Rails.application.config.session_store :cookie_store, key: '_rubybookshelf_session'
4 |
--------------------------------------------------------------------------------
/config/initializers/simple_form.rb:
--------------------------------------------------------------------------------
1 | # Use this setup block to configure all options available in SimpleForm.
2 | SimpleForm.setup do |config|
3 | # Wrappers are used by the form builder to generate a
4 | # complete input. You can remove any component from the
5 | # wrapper, change the order or even add your own to the
6 | # stack. The options given below are used to wrap the
7 | # whole input.
8 | config.wrappers :default, class: :input,
9 | hint_class: :field_with_hint, error_class: :field_with_errors do |b|
10 | ## Extensions enabled by default
11 | # Any of these extensions can be disabled for a
12 | # given input by passing: `f.input EXTENSION_NAME => false`.
13 | # You can make any of these extensions optional by
14 | # renaming `b.use` to `b.optional`.
15 |
16 | # Determines whether to use HTML5 (:email, :url, ...)
17 | # and required attributes
18 | b.use :html5
19 |
20 | # Calculates placeholders automatically from I18n
21 | # You can also pass a string as f.input placeholder: "Placeholder"
22 | b.use :placeholder
23 |
24 | ## Optional extensions
25 | # They are disabled unless you pass `f.input EXTENSION_NAME => true`
26 | # to the input. If so, they will retrieve the values from the model
27 | # if any exists. If you want to enable any of those
28 | # extensions by default, you can change `b.optional` to `b.use`.
29 |
30 | # Calculates maxlength from length validations for string inputs
31 | b.optional :maxlength
32 |
33 | # Calculates pattern from format validations for string inputs
34 | b.optional :pattern
35 |
36 | # Calculates min and max from length validations for numeric inputs
37 | b.optional :min_max
38 |
39 | # Calculates readonly automatically from readonly attributes
40 | b.optional :readonly
41 |
42 | ## Inputs
43 | b.use :label_input
44 | b.use :hint, wrap_with: { tag: :span, class: :hint }
45 | b.use :error, wrap_with: { tag: :span, class: :error }
46 |
47 | ## full_messages_for
48 | # If you want to display the full error message for the attribute, you can
49 | # use the component :full_error, like:
50 | #
51 | # b.use :full_error, wrap_with: { tag: :span, class: :error }
52 | end
53 |
54 | # The default wrapper to be used by the FormBuilder.
55 | config.default_wrapper = :default
56 |
57 | # Define the way to render check boxes / radio buttons with labels.
58 | # Defaults to :nested for bootstrap config.
59 | # inline: input + label
60 | # nested: label > input
61 | config.boolean_style = :nested
62 |
63 | # Default class for buttons
64 | config.button_class = 'btn'
65 |
66 | # Method used to tidy up errors. Specify any Rails Array method.
67 | # :first lists the first message for each field.
68 | # Use :to_sentence to list all errors for each field.
69 | # config.error_method = :first
70 |
71 | # Default tag used for error notification helper.
72 | config.error_notification_tag = :div
73 |
74 | # CSS class to add for error notification helper.
75 | config.error_notification_class = 'error_notification'
76 |
77 | # ID to add for error notification helper.
78 | # config.error_notification_id = nil
79 |
80 | # Series of attempts to detect a default label method for collection.
81 | # config.collection_label_methods = [ :to_label, :name, :title, :to_s ]
82 |
83 | # Series of attempts to detect a default value method for collection.
84 | # config.collection_value_methods = [ :id, :to_s ]
85 |
86 | # You can wrap a collection of radio/check boxes in a pre-defined tag, defaulting to none.
87 | # config.collection_wrapper_tag = nil
88 |
89 | # You can define the class to use on all collection wrappers. Defaulting to none.
90 | # config.collection_wrapper_class = nil
91 |
92 | # You can wrap each item in a collection of radio/check boxes with a tag,
93 | # defaulting to :span.
94 | # config.item_wrapper_tag = :span
95 |
96 | # You can define a class to use in all item wrappers. Defaulting to none.
97 | # config.item_wrapper_class = nil
98 |
99 | # How the label text should be generated altogether with the required text.
100 | # config.label_text = lambda { |label, required, explicit_label| "#{required} #{label}" }
101 |
102 | # You can define the class to use on all labels. Default is nil.
103 | # config.label_class = nil
104 |
105 | # You can define the default class to be used on forms. Can be overriden
106 | # with `html: { :class }`. Defaulting to none.
107 | # config.default_form_class = nil
108 |
109 | # You can define which elements should obtain additional classes
110 | # config.generate_additional_classes_for = [:wrapper, :label, :input]
111 |
112 | # Whether attributes are required by default (or not). Default is true.
113 | # config.required_by_default = true
114 |
115 | # Tell browsers whether to use the native HTML5 validations (novalidate form option).
116 | # These validations are enabled in SimpleForm's internal config but disabled by default
117 | # in this configuration, which is recommended due to some quirks from different browsers.
118 | # To stop SimpleForm from generating the novalidate option, enabling the HTML5 validations,
119 | # change this configuration to true.
120 | config.browser_validations = false
121 |
122 | # Collection of methods to detect if a file type was given.
123 | # config.file_methods = [ :mounted_as, :file?, :public_filename ]
124 |
125 | # Custom mappings for input types. This should be a hash containing a regexp
126 | # to match as key, and the input type that will be used when the field name
127 | # matches the regexp as value.
128 | # config.input_mappings = { /count/ => :integer }
129 |
130 | # Custom wrappers for input types. This should be a hash containing an input
131 | # type as key and the wrapper that will be used for all inputs with specified type.
132 | # config.wrapper_mappings = { string: :prepend }
133 |
134 | # Namespaces where SimpleForm should look for custom input classes that
135 | # override default inputs.
136 | # config.custom_inputs_namespaces << "CustomInputs"
137 |
138 | # Default priority for time_zone inputs.
139 | # config.time_zone_priority = nil
140 |
141 | # Default priority for country inputs.
142 | # config.country_priority = nil
143 |
144 | # When false, do not use translations for labels.
145 | # config.translate_labels = true
146 |
147 | # Automatically discover new inputs in Rails' autoload path.
148 | # config.inputs_discovery = true
149 |
150 | # Cache SimpleForm inputs discovery
151 | # config.cache_discovery = !Rails.env.development?
152 |
153 | # Default class for inputs
154 | # config.input_class = nil
155 |
156 | # Define the default class of the input wrapper of the boolean input.
157 | config.boolean_label_class = 'checkbox'
158 |
159 | # Defines if the default input wrapper class should be included in radio
160 | # collection wrappers.
161 | # config.include_default_input_wrapper_class = true
162 |
163 | # Defines which i18n scope will be used in Simple Form.
164 | # config.i18n_scope = 'simple_form'
165 | end
166 |
--------------------------------------------------------------------------------
/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # This file contains settings for ActionController::ParamsWrapper which
4 | # is enabled by default.
5 |
6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7 | ActiveSupport.on_load(:action_controller) do
8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters)
9 | end
10 |
11 | # To enable root element in JSON for ActiveRecord objects.
12 | # ActiveSupport.on_load(:active_record) do
13 | # self.include_root_in_json = true
14 | # end
15 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | en:
2 | date:
3 | formats:
4 | default:
5 | "%m/%d/%Y"
6 | with_weekday:
7 | "%a %m/%d/%y"
8 |
9 | time:
10 | formats:
11 | default:
12 | "%a, %b %-d, %Y at %r"
13 | date:
14 | "%b %-d, %Y"
15 | short:
16 | "%B %d"
17 |
18 | titles:
19 | application: "Ruby Bookshelf"
20 |
21 | books:
22 | index:
23 | header: "Books"
24 | no_books_yet: You haven't added any books yet.
25 | filter:
26 | button: "Filter"
27 | reviews:
28 | create_account: "Create an account to review this book."
29 |
30 | lists:
31 | list:
32 | empty: We haven't added any book yet.
33 | show:
34 | empty: This list is empty.
35 | index:
36 | header: "Lists"
37 | no_lists_yet: You haven't added any lists yet.
38 |
39 | authors:
40 | index:
41 | header: "Authors"
42 | no_authors_found: Sorry, but we don't have authors yet.
43 |
44 | registration:
45 | email: "Email"
46 | password: "Password"
47 | login: "Login"
48 | login_disable: "Logging in..."
49 | register: "Create account"
50 | register_disable: "Creating account..."
51 | success: "Thanks, %{email}! Welcome aboard"
52 | already_have_account: "Already have an account?"
53 | success: "Thanks, %{email}! You are now logged in"
54 |
55 | reviews:
56 | form:
57 | body: "Your review"
58 | rating: "Your rating"
59 | submit: "Submit review"
60 | submiting: "Submitting..."
61 | create:
62 | notice: "Great success! Your review is submitted"
63 |
64 | session:
65 | success: "Welcome, %{email}!"
66 | error: "Incorrect login information"
67 |
68 | application:
69 | title: "Ruby Bookshelf"
70 | header:
71 | registration: "Create your free account"
72 | login: "Login"
73 | logout: "Log out"
74 | menu:
75 | books: "Books"
76 | lists: "Lists"
77 | authors: "Authors"
78 | footer:
79 | navigate: "Navigate"
80 | follow_us: "Follow The Team"
81 | legal: "Legal"
82 | term: "Terms and Conditions"
83 | privacy: "Privacy Policy"
84 | sorting:
85 | by_title: "By title"
86 | newest_first: "Sort by newest first"
87 |
--------------------------------------------------------------------------------
/config/locales/simple_form.en.yml:
--------------------------------------------------------------------------------
1 | en:
2 | simple_form:
3 | "yes": 'Yes'
4 | "no": 'No'
5 | required:
6 | text: 'required'
7 | mark: '*'
8 | # You can uncomment the line below if you need to overwrite the whole required html.
9 | # When using html, text and mark won't be used.
10 | # html: '*'
11 | error_notification:
12 | default_message: "Please review the problems below:"
13 | # Examples
14 | # labels:
15 | # defaults:
16 | # password: 'Password'
17 | # user:
18 | # new:
19 | # email: 'E-mail to sign in.'
20 | # edit:
21 | # email: 'E-mail.'
22 | # hints:
23 | # defaults:
24 | # username: 'User name to sign in.'
25 | # password: 'No special characters, please.'
26 | # include_blanks:
27 | # defaults:
28 | # age: 'Rather not say'
29 | # prompts:
30 | # defaults:
31 | # age: 'Select your age'
32 |
--------------------------------------------------------------------------------
/config/newrelic.yml:
--------------------------------------------------------------------------------
1 | common: &default_settings
2 | app_name: "rubybookshelf"
3 | audit_log:
4 | enabled: false
5 | browser_monitoring:
6 | auto_instrument: true
7 | capture_params: false
8 | developer_mode: false
9 | error_collector:
10 | capture_source: true
11 | enabled: true
12 | ignore_errors: "ActionController::RoutingError,Sinatra::NotFound"
13 | license_key: "<%= ENV["NEW_RELIC_LICENSE_KEY"] %>"
14 | log_level: info
15 | monitor_mode: true
16 | transaction_tracer:
17 | enabled: true
18 | record_sql: obfuscated
19 | stack_trace_threshold: 0.500
20 | transaction_threshold: apdex_f
21 | development:
22 | <<: *default_settings
23 | monitor_mode: false
24 | developer_mode: true
25 | test:
26 | <<: *default_settings
27 | monitor_mode: false
28 | production:
29 | <<: *default_settings
30 | monitor_mode: true
31 | staging:
32 | <<: *default_settings
33 | app_name: "rubybookshelf (Staging)"
34 | monitor_mode: true
35 |
--------------------------------------------------------------------------------
/config/puma.rb:
--------------------------------------------------------------------------------
1 | # https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server
2 |
3 | # The environment variable WEB_CONCURRENCY may be set to a default value based
4 | # on dyno size. To manually configure this value use heroku config:set
5 | # WEB_CONCURRENCY.
6 | #
7 | # Increasing the number of workers will increase the amount of resting memory
8 | # your dynos use. Increasing the number of threads will increase the amount of
9 | # potential bloat added to your dynos when they are responding to heavy
10 | # requests.
11 | #
12 | # Starting with a low number of workers and threads provides adequate
13 | # performance for most applications, even under load, while maintaining a low
14 | # risk of overusing memory.
15 | workers Integer(ENV.fetch("WEB_CONCURRENCY", 2))
16 | threads_count = Integer(ENV.fetch("MAX_THREADS", 2))
17 | threads(threads_count, threads_count)
18 |
19 | preload_app!
20 |
21 | rackup DefaultRackup
22 | environment ENV.fetch("RACK_ENV", "development")
23 |
24 | on_worker_boot do
25 | # Worker specific setup for Rails 4.1+
26 | # See: https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server#on-worker-boot
27 | ActiveRecord::Base.establish_connection
28 | end
29 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | root "pages#home"
3 |
4 | resources :books, only: [:index, :show] do
5 | collection do
6 | resource :filter, only: :show, as: :book_filter, controller: "books/filter"
7 | end
8 | resources :reviews, only: [:create]
9 | end
10 |
11 | resources :lists, only: [:index, :show]
12 | resources :authors, only: [:index, :show]
13 | resource :registration, only: [:new, :create]
14 | resource :session, only: [:create, :destroy]
15 |
16 | get "/login", to: "sessions#new", as: :login
17 | end
18 |
--------------------------------------------------------------------------------
/config/secrets.yml:
--------------------------------------------------------------------------------
1 | default: &default
2 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
3 |
4 | development:
5 | <<: *default
6 |
7 | test:
8 | <<: *default
9 |
10 | staging:
11 | <<: *default
12 |
13 | production:
14 | <<: *default
15 |
--------------------------------------------------------------------------------
/config/smtp.rb:
--------------------------------------------------------------------------------
1 | SMTP_SETTINGS = {
2 | address: ENV.fetch("SMTP_ADDRESS"), # example: "smtp.sendgrid.net"
3 | authentication: :plain,
4 | domain: ENV.fetch("SMTP_DOMAIN"), # example: "heroku.com"
5 | enable_starttls_auto: true,
6 | password: ENV.fetch("SMTP_PASSWORD"),
7 | port: "587",
8 | user_name: ENV.fetch("SMTP_USERNAME")
9 | }
10 |
--------------------------------------------------------------------------------
/db/migrate/20151024110515_create_delayed_jobs.rb:
--------------------------------------------------------------------------------
1 | class CreateDelayedJobs < ActiveRecord::Migration
2 | def self.up
3 | create_table :delayed_jobs, force: true do |table|
4 | table.integer :priority, default: 0, null: false # Allows some jobs to jump to the front of the queue
5 | table.integer :attempts, default: 0, null: false # Provides for retries, but still fail eventually.
6 | table.text :handler, null: false # YAML-encoded string of the object that will do work
7 | table.text :last_error # reason for last failure (See Note below)
8 | table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future.
9 | table.datetime :locked_at # Set when a client is working on this object
10 | table.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead)
11 | table.string :locked_by # Who is working on this object (if locked)
12 | table.string :queue # The name of the queue this job is in
13 | table.timestamps null: true
14 | end
15 |
16 | add_index :delayed_jobs, [:priority, :run_at], name: "delayed_jobs_priority"
17 | end
18 |
19 | def self.down
20 | drop_table :delayed_jobs
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/db/migrate/20151024161110_create_books.rb:
--------------------------------------------------------------------------------
1 | class CreateBooks < ActiveRecord::Migration
2 | def change
3 | create_table :books do |t|
4 | t.string :title, index: true, null: false
5 |
6 | t.timestamps null: false
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/db/migrate/20151025083545_add_summary_to_books.rb:
--------------------------------------------------------------------------------
1 | class AddSummaryToBooks < ActiveRecord::Migration
2 | def change
3 | add_column :books, :summary, :text
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20151025170319_create_genres.rb:
--------------------------------------------------------------------------------
1 | class CreateGenres < ActiveRecord::Migration
2 | def change
3 | create_table :genres do |t|
4 | t.string :name, null: false
5 |
6 | t.timestamps null: false
7 | end
8 |
9 | add_index :genres, :name, unique: true
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/db/migrate/20151025170953_add_genre_to_books.rb:
--------------------------------------------------------------------------------
1 | class AddGenreToBooks < ActiveRecord::Migration
2 | def change
3 | add_reference :books, :genre, index: true
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20151026202920_create_lists.rb:
--------------------------------------------------------------------------------
1 | class CreateLists < ActiveRecord::Migration
2 | # TODO: any indices needed?
3 | def change
4 | create_table :lists do |t|
5 | t.string :name, null: false
6 |
7 | t.timestamps null: false
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/db/migrate/20151026205814_create_list_entries.rb:
--------------------------------------------------------------------------------
1 | class CreateListEntries < ActiveRecord::Migration
2 | def change
3 | create_table :list_entries do |t|
4 | t.references :list, index: true, foreign_key: true, null: false
5 | t.references :book, index: true, foreign_key: true, null: false
6 |
7 | t.timestamps null: false, index: true
8 | end
9 |
10 | add_index :list_entries, [:list_id, :book_id], unique: true
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/db/migrate/20151027020720_create_authors.rb:
--------------------------------------------------------------------------------
1 | class CreateAuthors < ActiveRecord::Migration
2 | def change
3 | create_table :authors do |t|
4 | t.string :name, index: true, null: false
5 |
6 | t.timestamps null: false
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/db/migrate/20151111024028_add_description_to_author.rb:
--------------------------------------------------------------------------------
1 | class AddDescriptionToAuthor < ActiveRecord::Migration
2 | def change
3 | add_column :authors, :description, :text
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20151111024650_add_author_to_books.rb:
--------------------------------------------------------------------------------
1 | class AddAuthorToBooks < ActiveRecord::Migration
2 | def change
3 | add_reference :books, :author, index: true
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20151111033154_add_cover_to_book.rb:
--------------------------------------------------------------------------------
1 | class AddCoverToBook < ActiveRecord::Migration
2 | def change
3 | add_column :books, :cover, :string
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20151111234610_create_users.rb:
--------------------------------------------------------------------------------
1 | class CreateUsers < ActiveRecord::Migration
2 | def change
3 | create_table :users do |t|
4 | t.string :email, null: false
5 | t.string :password_digest, null: false
6 |
7 | t.timestamps null: false
8 | end
9 | add_index :users, :email, unique: true
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/db/migrate/20151114125209_create_friendly_id_slugs.rb:
--------------------------------------------------------------------------------
1 | class CreateFriendlyIdSlugs < ActiveRecord::Migration
2 | def change
3 | add_column :books, :slug, :string
4 | add_column :authors, :slug, :string
5 | add_column :lists, :slug, :string
6 |
7 | add_index :books, :slug, unique: true
8 | add_index :authors, :slug, unique: true
9 | add_index :lists, :slug, unique: true
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/db/migrate/20151114145753_add_released_on_to_books.rb:
--------------------------------------------------------------------------------
1 | class AddReleasedOnToBooks < ActiveRecord::Migration
2 | def change
3 | add_column :books, :released_on, :date, index: :true
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20151115085750_create_reviews.rb:
--------------------------------------------------------------------------------
1 | class CreateReviews < ActiveRecord::Migration
2 | def change
3 | create_table :reviews do |t|
4 | t.text :body, null: false
5 | t.references :book, index: true, foreign_key: true
6 | t.references :user, index: true, foreign_key: true
7 |
8 | t.timestamps null: false
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/db/migrate/20151115205953_add_rating_to_reviews.rb:
--------------------------------------------------------------------------------
1 | class AddRatingToReviews < ActiveRecord::Migration
2 | def change
3 | add_column :reviews, :rating, :integer
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/schema.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | # This file is auto-generated from the current state of the database. Instead
3 | # of editing this file, please use the migrations feature of Active Record to
4 | # incrementally modify your database, and then regenerate this schema definition.
5 | #
6 | # Note that this schema.rb definition is the authoritative source for your
7 | # database schema. If you need to create the application database on another
8 | # system, you should be using db:schema:load, not running all the migrations
9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations
10 | # you'll amass, the slower it'll run and the greater likelihood for issues).
11 | #
12 | # It's strongly recommended that you check this file into your version control system.
13 |
14 | ActiveRecord::Schema.define(version: 20151115205953) do
15 |
16 | # These are extensions that must be enabled in order to support this database
17 | enable_extension "plpgsql"
18 |
19 | create_table "authors", force: :cascade do |t|
20 | t.string "name", null: false
21 | t.datetime "created_at", null: false
22 | t.datetime "updated_at", null: false
23 | t.text "description"
24 | t.string "slug"
25 | end
26 |
27 | add_index "authors", ["name"], name: "index_authors_on_name", using: :btree
28 | add_index "authors", ["slug"], name: "index_authors_on_slug", unique: true, using: :btree
29 |
30 | create_table "books", force: :cascade do |t|
31 | t.string "title", null: false
32 | t.datetime "created_at", null: false
33 | t.datetime "updated_at", null: false
34 | t.text "summary"
35 | t.integer "genre_id"
36 | t.integer "author_id"
37 | t.string "cover"
38 | t.date "released_on"
39 | t.string "slug"
40 | end
41 |
42 | add_index "books", ["author_id"], name: "index_books_on_author_id", using: :btree
43 | add_index "books", ["genre_id"], name: "index_books_on_genre_id", using: :btree
44 | add_index "books", ["slug"], name: "index_books_on_slug", unique: true, using: :btree
45 | add_index "books", ["title"], name: "index_books_on_title", using: :btree
46 |
47 | create_table "delayed_jobs", force: :cascade do |t|
48 | t.integer "priority", default: 0, null: false
49 | t.integer "attempts", default: 0, null: false
50 | t.text "handler", null: false
51 | t.text "last_error"
52 | t.datetime "run_at"
53 | t.datetime "locked_at"
54 | t.datetime "failed_at"
55 | t.string "locked_by"
56 | t.string "queue"
57 | t.datetime "created_at"
58 | t.datetime "updated_at"
59 | end
60 |
61 | add_index "delayed_jobs", ["priority", "run_at"], name: "delayed_jobs_priority", using: :btree
62 |
63 | create_table "genres", force: :cascade do |t|
64 | t.string "name", null: false
65 | t.datetime "created_at", null: false
66 | t.datetime "updated_at", null: false
67 | end
68 |
69 | add_index "genres", ["name"], name: "index_genres_on_name", unique: true, using: :btree
70 |
71 | create_table "list_entries", force: :cascade do |t|
72 | t.integer "list_id", null: false
73 | t.integer "book_id", null: false
74 | t.datetime "created_at", null: false
75 | t.datetime "updated_at", null: false
76 | end
77 |
78 | add_index "list_entries", ["book_id"], name: "index_list_entries_on_book_id", using: :btree
79 | add_index "list_entries", ["created_at"], name: "index_list_entries_on_created_at", using: :btree
80 | add_index "list_entries", ["list_id", "book_id"], name: "index_list_entries_on_list_id_and_book_id", unique: true, using: :btree
81 | add_index "list_entries", ["list_id"], name: "index_list_entries_on_list_id", using: :btree
82 | add_index "list_entries", ["updated_at"], name: "index_list_entries_on_updated_at", using: :btree
83 |
84 | create_table "lists", force: :cascade do |t|
85 | t.string "name", null: false
86 | t.datetime "created_at", null: false
87 | t.datetime "updated_at", null: false
88 | t.string "slug"
89 | end
90 |
91 | add_index "lists", ["slug"], name: "index_lists_on_slug", unique: true, using: :btree
92 |
93 | create_table "reviews", force: :cascade do |t|
94 | t.text "body", null: false
95 | t.integer "book_id"
96 | t.integer "user_id"
97 | t.datetime "created_at", null: false
98 | t.datetime "updated_at", null: false
99 | t.integer "rating"
100 | end
101 |
102 | add_index "reviews", ["book_id"], name: "index_reviews_on_book_id", using: :btree
103 | add_index "reviews", ["user_id"], name: "index_reviews_on_user_id", using: :btree
104 |
105 | create_table "users", force: :cascade do |t|
106 | t.string "email", null: false
107 | t.string "password_digest", null: false
108 | t.datetime "created_at", null: false
109 | t.datetime "updated_at", null: false
110 | end
111 |
112 | add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
113 |
114 | add_foreign_key "list_entries", "books"
115 | add_foreign_key "list_entries", "lists"
116 | add_foreign_key "reviews", "books"
117 | add_foreign_key "reviews", "users"
118 | end
119 |
--------------------------------------------------------------------------------
/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # This file should contain all the record creation needed to seed the database with its default values.
2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
3 | #
4 | # Examples:
5 | #
6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }])
7 | # Mayor.create(name: 'Emanuel', city: cities.first)
8 |
--------------------------------------------------------------------------------
/lib/tasks/add_slugs_to_existing_records.rake:
--------------------------------------------------------------------------------
1 | desc "Add slugs to existing records"
2 | task add_slugs_to_exising_records: :environment do
3 | [Book, List, Author].each do |klass|
4 | klass.find_each(&:save)
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/lib/tasks/bundler_audit.rake:
--------------------------------------------------------------------------------
1 | if Rails.env.development? || Rails.env.test?
2 | require "bundler/audit/cli"
3 |
4 | namespace :bundler do
5 | desc "Updates the ruby-advisory-db and runs audit"
6 | task :audit do
7 | %w(update check).each do |command|
8 | Bundler::Audit::CLI.start [command]
9 | end
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/lib/tasks/dev.rake:
--------------------------------------------------------------------------------
1 | if Rails.env.development? || Rails.env.test?
2 | require "factory_girl"
3 |
4 | namespace :dev do
5 | desc "Sample data for local development environment"
6 | task prime: "db:setup" do
7 | include FactoryGirl::Syntax::Methods
8 |
9 | jrr = create(:author, name: "J. R. R. Tolkien", description: "J.R.R. Tolkien (1892.1973), beloved throughout the world as the creator of The Hobbit and The Lord of the Rings, was a professor of Anglo-Saxon at Oxford, a fellow of Pembroke College, and a fellow of Merton College until his retirement in 1959.")
10 | dave = create(:author, name: "Dave Thomas", description: "Dave Thomas (@pragdave) is a cornerstone of the Ruby community, and is personally responsible for many of its innovative directions and initiatives. Dave is a programmer, and now he is an accidental publisher.")
11 |
12 | fantasy = create(:genre, name: "Fantasy")
13 | programming = create(:genre, name: "Programming")
14 | create(:genre, name: "Science")
15 |
16 | fellowship = create(:book,
17 | title: "The Fellowship of the Ring",
18 | released_on: "1954-07-29",
19 | genre: fantasy, author: jrr, cover: "http://ecx.images-amazon.com/images/I/51wIJN44zhL.jpg",
20 | summary: "The dark, fearsome Ringwraiths are searching for a Hobbit. Frodo Baggins knows that they are seeking him and the Ring he bears—the Ring of Power that will enable evil Sauron to destroy all that is good in Middle-earth. Now it is up to Frodo and his faithful servant, Sam, with a small band of companions, to carry the Ring to the one place it can be destroyed: Mount Doom, in the very center of Sauron’s realm.")
21 | two_towers = create(:book,
22 | title: "The Two Towers",
23 | released_on: "1954-11-11",
24 | genre: fantasy,
25 | author: jrr,
26 | cover: "http://ecx.images-amazon.com/images/I/51HUqZm3JTL.jpg", summary: "The Fellowship is scattered. Some are bracing hopelessly for war against the ancient evil of Sauron. Some are contending with the treachery of the wizard Saruman. Only Frodo and Sam are left to take the accursed One Ring, ruler of all the Rings of Power, to be destroyed in Mordor, the dark realm where Sauron is supreme. Their guide is Gollum, deceitful and lust-filled, slave to the corruption of the Ring.")
27 | create(:book,
28 | title: "Programming Ruby",
29 | genre: programming,
30 | author: dave,
31 | cover: "http://ecx.images-amazon.com/images/I/41gtODXuRlL.jpg",
32 | summary: "Ruby, a new, object-oriented scripting language, has won over thousands of Perl and Python programmers in Japan -- and it's now launching worldwide. This is the world's first English-language developer's guide to Ruby. Written by the two leading Ruby developers, Programming Ruby demonstrates Ruby's compelling advantages, and serves as a start-to-finish tutorial and reference for every developer.")
33 | create(:book, title: "Oxford English Dictionary")
34 |
35 |
36 | create(:list, name: "Empty List")
37 | best_fantasy = create(:list, name: "Best Fantasy")
38 |
39 | create(:list_entry, book: fellowship, list: best_fantasy)
40 | create(:list_entry, book: two_towers, list: best_fantasy)
41 |
42 | # for testing pagination
43 | create_list(:book, 25, genre: fantasy, summary: "Alright, when you into a movie theatre in Amsterdam, you can buy beer. And I don't mean in a paper cup either. They give you a glass of beer. And in Paris, you can buy beer at MacDonald's. And you know what they call a Quarter Pounder with Cheese in Paris? They don't call it a Quarter Pounder with Cheese? No, they got the metric system there, they wouldn't know what the fuck a Quarter Pounder is. What'd they call it? They call it Royale with Cheese.")
44 | end
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/lib/templates/erb/scaffold/_form.html.erb:
--------------------------------------------------------------------------------
1 | <%%= simple_form_for(@<%= singular_table_name %>) do |f| %>
2 | <%%= f.error_notification %>
3 |
4 |
5 | <%- attributes.each do |attribute| -%>
6 | <%%= f.<%= attribute.reference? ? :association : :input %> :<%= attribute.name %> %>
7 | <%- end -%>
8 |
9 |
10 |
11 | <%%= f.button :submit %>
12 |
13 | <%% end %>
14 |
--------------------------------------------------------------------------------
/log/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taylorrf/rubybookshelf/8b94ee49cb6ebec4fcf567bb66298604c07dddfe/log/.keep
--------------------------------------------------------------------------------
/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | The page you were looking for doesn't exist (404)
8 |
9 |
58 |
59 |
60 |
61 |
62 |
63 |
The page you were looking for doesn't exist.
64 |
You may have mistyped the address or the page may have moved.
65 |
66 |
If you are the application owner check the logs for more information.
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | The change you wanted was rejected (422)
8 |
9 |
58 |
59 |
60 |
61 |
62 |
63 |
The change you wanted was rejected.
64 |
Maybe you tried to change something you didn't have access to.
65 |
66 |
If you are the application owner check the logs for more information.
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | We're sorry, but something went wrong (500)
8 |
9 |
58 |
59 |
60 |
61 |
62 |
63 |
We're sorry, but something went wrong.
64 |
65 |
If you are the application owner check the logs for more information.
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taylorrf/rubybookshelf/8b94ee49cb6ebec4fcf567bb66298604c07dddfe/public/favicon.ico
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-agent: *
5 | # Disallow: /
6 |
--------------------------------------------------------------------------------
/spec/controllers/registrations_controller_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | RSpec.describe RegistrationsController do
4 | describe "POST create" do
5 | it "redirects to root path if registration succeeds" do
6 | stub_user_creation_with method: :save, return_value: true
7 |
8 | post :create, registration: attributes_for(:user)
9 |
10 | expect(response).to redirect_to root_url
11 | end
12 |
13 | it "renders new if registration fails" do
14 | stub_user_creation_with method: :save, return_value: false
15 |
16 | post :create, registration: { email: "" }
17 |
18 | expect(response).to render_template :new
19 | end
20 |
21 | it "signs the user in" do
22 | user = stub_user_creation_with method: :save, return_value: true
23 |
24 | post :create, registration: { email: "" }
25 |
26 | expect(session[:user_id]).to eq(user.id)
27 | end
28 | end
29 |
30 | def stub_user_creation_with(method:, return_value:)
31 | build_stubbed(:user).tap do |stubbed_user|
32 | allow(User).to receive(:new).and_return stubbed_user
33 | allow(stubbed_user).to receive(method).and_return return_value
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/spec/controllers/reviews_controller_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | RSpec.describe ReviewsController, type: :controller do
4 | describe "#create" do
5 | context "when the review is valid" do
6 | it "redirects to the book path" do
7 | book = stub_book
8 | stub_save_review(success: true)
9 |
10 | post :create, book_id: book.id, review: { body: "" }
11 |
12 | expect(response).to redirect_to book_path(book.id)
13 | end
14 |
15 | it "sets the flash notice" do
16 | book = stub_book
17 | stub_save_review(success: true)
18 |
19 | post :create, book_id: book.id, review: { body: "" }
20 |
21 | expect(flash[:notice]).to eq(t("reviews.create.notice"))
22 | end
23 | end
24 |
25 | context "when the review is invalid" do
26 | it "renders the book page" do
27 | book = stub_book
28 | stub_save_review(success: false)
29 |
30 | post :create, book_id: book.id, review: { body: "" }
31 |
32 | expect(response).to render_template("books/show")
33 | end
34 | end
35 | end
36 |
37 | def stub_signed_in_user
38 | user = double(:user)
39 | allow(controller).to receive(:current_user).and_return(user)
40 | end
41 |
42 | def stub_book
43 | build_stubbed(:book).tap do |book|
44 | allow(Book).to receive(:find).and_return(book)
45 | end
46 | end
47 |
48 | def stub_save_review(success:)
49 | instance_double("Review", save: success).tap do |review|
50 | allow(Review).to receive(:new).and_return(review)
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/spec/controllers/sessions_controller_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | RSpec.describe SessionsController do
4 | describe "POST create" do
5 | it "redirects to root url on successful authentication" do
6 | stub_session_authentication method: :authenticate, return_value: true
7 |
8 | post :create, session: attributes_for(:user)
9 |
10 | expect(response).to redirect_to root_url
11 | end
12 |
13 | it "renders new on unsuccessful authentication" do
14 | stub_session_authentication method: :authenticate, return_value: false
15 |
16 | post :create, session: { email: "invalidemail", password: "somepassword" }
17 |
18 | expect(response).to render_template :new
19 | end
20 | end
21 |
22 | def stub_session_authentication(method:, return_value:)
23 | build_stubbed(:user).tap do |stubbed_user|
24 | allow(User).to receive(:lookup_for_authentication_with).
25 | and_return stubbed_user
26 | allow(stubbed_user).to receive(method).and_return(return_value)
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/spec/facades/book_facade_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | RSpec.describe BookFacade do
4 | describe "#book" do
5 | it "returns the book" do
6 | book = double(:book)
7 | user = double(:user)
8 | facade = BookFacade.new(book: book, current_user: user)
9 |
10 | result = facade.book
11 |
12 | expect(result).to eq(book)
13 | end
14 | end
15 |
16 | describe "#review_policy" do
17 | it "returns the appropriate policy" do
18 | # lots of stubbing needed here, maybe there's a better way?
19 | reviewer = double(:reviewer)
20 | review = double(:review, reviewer: reviewer)
21 | book = double(:book, reviews: [review])
22 | user = double(:user)
23 | policy = double(:policy)
24 | allow(ReviewPolicy).to receive(:new).with(
25 | reviewers: [reviewer],
26 | reviewer_to_auth: user
27 | ).and_return(policy)
28 | facade = BookFacade.new(book: book, current_user: user)
29 |
30 | result = facade.review_policy
31 |
32 | expect(result).to eq(policy)
33 | end
34 | end
35 |
36 | describe "#new_review" do
37 | it "returns a new Review" do
38 | book = double(:book)
39 | user = double(:user)
40 | facade = BookFacade.new(book: book, current_user: user)
41 |
42 | result = facade.new_review
43 |
44 | expect(result).to be_a(Review)
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/spec/factories.rb:
--------------------------------------------------------------------------------
1 | FactoryGirl.define do
2 | factory :author do
3 | name "Uncle Bob"
4 | end
5 |
6 | factory :book do
7 | sequence(:title) { |n| "Default Book Title #{n}" }
8 | released_on Time.zone.today
9 |
10 | trait :no_genre do
11 | genre nil
12 | end
13 | end
14 |
15 | factory :genre do
16 | name "Default Genre"
17 | end
18 |
19 | factory :list do
20 | name "Default List Name"
21 | end
22 |
23 | factory :list_entry do
24 | book
25 | list
26 | end
27 |
28 | factory :review do
29 | body "A review"
30 | end
31 |
32 | factory :user do
33 | sequence(:email) { |n| "user_#{n}@example.com" }
34 | password "examplepassword"
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/spec/features/filter_books_by_genre_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | RSpec.feature "Filter Books by Genre", type: :feature do
4 | # TODO: what about filter by "No Genre"?
5 | scenario do
6 | create(:book, title: "The Fellowship of the Ring", genre: fantasy)
7 | create(:book, title: "Programming Ruby", genre: programming)
8 |
9 | visit books_path
10 | apply_filter "Science Fiction"
11 |
12 | expect(book_list).to include("The Fellowship of the Ring")
13 | expect(book_list).to_not include("Programming Ruby")
14 | end
15 |
16 | def apply_filter(genre)
17 | select(genre, from: "Genre")
18 | click_on "Filter"
19 | end
20 |
21 | def book_list
22 | page.all(".book").map(&:text)
23 | end
24 |
25 | def fantasy
26 | @fantasy ||= create(:genre, name: "Science Fiction")
27 | end
28 |
29 | def programming
30 | @programming ||= create(:genre, name: "Programming")
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/spec/features/pagination_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | RSpec.feature "Pagination", type: :feature do
4 | scenario "less than one page of books" do
5 | create_list(:book, 2)
6 |
7 | visit books_path
8 |
9 | expect(page).to have_displayed_books(2)
10 | expect(page).to_not have_pagination_links
11 | end
12 |
13 | scenario "viewing the first page of books" do
14 | create_list(:book, 22)
15 |
16 | visit books_path
17 |
18 | expect(page).to have_displayed_books(20)
19 | end
20 |
21 | scenario "paginating through the books" do
22 | create_list(:book, 22)
23 |
24 | visit books_path
25 | click_link "Next"
26 |
27 | expect(page).to have_displayed_books(2)
28 | end
29 |
30 | def have_displayed_books(count)
31 | have_css(".books .book", count: count)
32 | end
33 |
34 | def have_pagination_links
35 | have_css("nav.pagination")
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/spec/features/show_all_of_the_lists_of_books_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | RSpec.describe "Shows all of the lists of books", type: :feature do
4 | scenario "list of lists" do
5 | create(:list, name: "Best Fantasy")
6 | create(:list, name: "Best Science Fiction")
7 |
8 | visit lists_path
9 |
10 | expect(page).to have_list("Best Fantasy")
11 | expect(page).to have_list("Best Science Fiction")
12 | end
13 |
14 | scenario "books within a list" do
15 | best_fantasy = create(:list, name: "Best Fantasy")
16 | fellowship = create(:book, cover: "http://cov.er/fellowship_ring.jpg")
17 | two_towers = create(:book, cover: "http://cov.er/two_towers.jpg")
18 | create(:list_entry, list: best_fantasy, book: fellowship)
19 | create(:list_entry, list: best_fantasy, book: two_towers)
20 |
21 | visit lists_path
22 |
23 | expect(page).to have_list("Best Fantasy")
24 | expect(page).to have_book_cover("http://cov.er/fellowship_ring.jpg")
25 | expect(page).to have_book_cover("http://cov.er/two_towers.jpg")
26 | end
27 |
28 | def have_list(name)
29 | have_css(".lists-index h2", text: name)
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/spec/features/sort_by_date_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | RSpec.feature "Sort by date", type: :feature do
4 | scenario do
5 | create(:book, released_on: 1.day.ago, title: "The Fellowship of the Ring")
6 | create(:book, released_on: 1.month.ago, title: "The Return of the King")
7 | create(:book, released_on: 1.week.ago, title: "The Two Towers")
8 |
9 | visit books_path
10 | click_link t("sorting.newest_first")
11 |
12 | expect(book_list).to eq([
13 | "The Fellowship of the Ring",
14 | "The Two Towers",
15 | "The Return of the King"
16 | ])
17 | end
18 |
19 | def book_list
20 | page.all(".book").map(&:text)
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/spec/features/user_logs_in_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | RSpec.describe "User logs in" do
4 | scenario "with known credentials" do
5 | user = create :user
6 |
7 | visit root_path
8 | login_as user
9 |
10 | expect(page).to have_content t("session.success", email: user.email)
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/spec/features/user_logs_out_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | RSpec.feature "User logs out" do
4 | scenario "to end his session on the website" do
5 | user = create :user
6 |
7 | login_as user
8 | click_link t("application.header.logout")
9 |
10 | expect(page).to have_link t("application.header.login"), href: login_path
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/spec/features/user_writes_a_review_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | RSpec.feature "User writes a review", type: :feature do
4 | scenario "if he has written no previous review for a book" do
5 | create(
6 | :book,
7 | title: "The Shades: a history",
8 | reviews: []
9 | )
10 |
11 | user = create :user
12 |
13 | login_as user
14 | within "header.header-2" do
15 | click_link "Books"
16 | end
17 | click_link "The Shades: a history"
18 | fill_in t("reviews.form.body"),
19 | with: "They missed the Dragon's Den. Unbelievable"
20 | select "4", from: "Your rating"
21 | click_on t("reviews.form.submit")
22 |
23 | expect(page).to have_content user.email
24 | expect(page).to have_content "They missed the Dragon's Den. Unbelievable"
25 | expect(page).to have_content "Rating: 4"
26 | end
27 |
28 | scenario "if he has written a review for the book already" do
29 | reviewer = create :user
30 | create(
31 | :book,
32 | title: "The Shades: a history",
33 | reviews: [create(:review, reviewer: reviewer)]
34 | )
35 |
36 | login_as reviewer
37 | click_link "Books", match: :first
38 | click_link "The Shades: a history"
39 |
40 | expect(page).not_to have_content t("reviews.form.body")
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/spec/features/view_a_single_book_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | RSpec.feature "View a single book", type: :feature do
4 | scenario do
5 | create(:book,
6 | title: "The Fellowship of the Ring",
7 | summary: "The first book in the series",
8 | cover: "fellowship_ring.jpg")
9 |
10 | visit books_path
11 | click_link("The Fellowship of the Ring")
12 |
13 | expect(page).to have_book_title("The Fellowship of the Ring")
14 | expect(page).to have_book_summary("The first book in the series")
15 | expect(page).to have_book_cover("fellowship_ring.jpg")
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/spec/features/view_an_individual_author_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | RSpec.feature "View an individual author", type: :feature do
4 | scenario do
5 | author = create(:author, name: "Sandi Metz", description: "developer")
6 | book = create(:book, author: author, cover: "pratical.jpg")
7 |
8 | visit authors_path
9 | click_link("Sandi Metz")
10 |
11 | expect(page).to have_css("h1", text: "Sandi Metz")
12 | expect(page).to have_css("p", text: author.description)
13 | expect(page).to have_book_title(book.title)
14 | expect(page).to have_book_cover(book.cover)
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/spec/features/view_an_individual_list_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | RSpec.feature "View an individual list", type: :feature do
4 | scenario do
5 | list = create(:list, name: "Best Fantasy")
6 | add_book_to_list(title: "The Fellowship of the Ring", list: list)
7 | add_book_to_list(title: "The Two Towers", list: list)
8 |
9 | visit lists_path
10 | click_link("Best Fantasy")
11 |
12 | expect(page).to have_heading("Best Fantasy")
13 | expect(page).to have_book("The Fellowship of the Ring")
14 | expect(page).to have_book("The Two Towers")
15 | end
16 |
17 | def have_heading(title)
18 | have_css("h1", text: title)
19 | end
20 |
21 | def have_book(title)
22 | have_css(".book h1", text: title)
23 | end
24 |
25 | def add_book_to_list(title:, list:)
26 | book = create(:book, title: title)
27 | create(:list_entry, book: book, list: list)
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/spec/features/view_list_of_all_books_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | RSpec.feature "List all books", type: :feature do
4 | scenario "when there are some books to list" do
5 | create(:book, title: "The Fellowship of the Ring")
6 | create(:book, title: "The Two Towers")
7 |
8 | visit books_path
9 |
10 | expect(page).to have_book("The Fellowship of the Ring")
11 | expect(page).to have_book("The Two Towers")
12 | end
13 |
14 | def have_book(title)
15 | have_css(".book", text: title)
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/spec/features/view_list_of_authors_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | RSpec.feature "View list of Authors", type: :feature do
4 | scenario "with some authors" do
5 | create(:author, name: "Martin Fowler")
6 | uncle = create(:author, name: "Uncle Bob")
7 |
8 | create(:book, author: uncle, cover: "clean_code.jpg")
9 | create(:book, author: uncle, cover: "clean_coders.jpg")
10 |
11 | visit authors_path
12 |
13 | expect(page).to have_author("Martin Fowler")
14 | expect(page).to have_author("Uncle Bob")
15 | expect(page).to have_book_cover("clean_code.jpg")
16 | expect(page).to have_book_cover("clean_coders.jpg")
17 | end
18 |
19 | scenario "without authors" do
20 | visit authors_path
21 | expect(page).to have_content(t("authors.index.no_authors_found"))
22 | end
23 |
24 | def have_author(name)
25 | have_css(".authors-index h2", text: name)
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/spec/features/visitor_creates_account_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | RSpec.feature "Visitor registers account" do
4 | scenario "to become a member of the site" do
5 | visit root_path
6 |
7 | click_link t("application.header.registration")
8 | fill_in t("registration.email"), with: "example_user@example.com"
9 | fill_in t("registration.password"), with: "examplepassword"
10 | click_button t("registration.register")
11 |
12 | expect(page).to have_content(
13 | t("registration.success", email: "example_user@example.com")
14 | )
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/spec/forms/filter_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | RSpec.describe Filter do
4 | describe "#genres" do
5 | it "delegates to `Genre`" do
6 | filter = Filter.new
7 | genres = double(:genres)
8 | allow(Genre).to receive(:containing_books).and_return(genres)
9 |
10 | result = filter.genres
11 |
12 | expect(result).to eq(genres)
13 | end
14 | end
15 |
16 | describe "#matches" do
17 | it "returns only books in that genre" do
18 | _book_without_genre = create(:book, :no_genre)
19 | fantasy_book = create(:book, genre: fantasy_genre)
20 | _programming_book = create(:book, genre: programming_genre)
21 |
22 | result = Filter.new(genre_id: fantasy_genre.id).matches
23 |
24 | expect(result).to eq([fantasy_book])
25 | end
26 | end
27 |
28 | def fantasy_genre
29 | @fantasy_genre ||= create(:genre, name: "Fantasy")
30 | end
31 |
32 | def programming_genre
33 | @programming_genre ||= create(:genre, name: "Programming")
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/spec/helpers/books_helper_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | RSpec.describe BooksHelper, type: :helper do
4 | describe "#sort_path" do
5 | it "returns the standard books_paths when there's on args" do
6 | stub_params
7 |
8 | result = helper.sort_path
9 |
10 | expect(result).to eq(books_path)
11 | end
12 |
13 | it "retains the current sort order" do
14 | stub_params
15 |
16 | result = helper.sort_path(Book::SORT_CRITERIA_RELEASE_DATE)
17 |
18 | expect(result).to eq(books_path(sort: Book::SORT_CRITERIA_RELEASE_DATE))
19 | end
20 |
21 | it "retains the current page number" do
22 | stub_params(page: "page-number")
23 |
24 | result = helper.sort_path
25 |
26 | expect(result).to eq(books_path(page: "page-number"))
27 | end
28 |
29 | it "retains the current filter" do
30 | stub_params(filter: "fantasy")
31 |
32 | result = helper.sort_path
33 |
34 | expect(result).to eq(books_path(filter: "fantasy"))
35 | end
36 |
37 | def stub_params(args = {})
38 | allow(helper).to receive(:params).and_return(
39 | {
40 | controller: "books",
41 | action: "index"
42 | }.merge(args)
43 | )
44 | end
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/spec/i18n_spec.rb:
--------------------------------------------------------------------------------
1 | require 'i18n/tasks'
2 |
3 | RSpec.describe 'I18n' do
4 | let(:i18n) { I18n::Tasks::BaseTask.new }
5 | let(:missing_keys) { i18n.missing_keys }
6 | let(:unused_keys) { i18n.unused_keys }
7 |
8 | it 'does not have missing keys' do
9 | expect(missing_keys).to be_empty,
10 | "Missing #{missing_keys.leaves.count} i18n keys, run `i18n-tasks missing' to show them"
11 | end
12 |
13 | it 'does not have unused keys' do
14 | expect(unused_keys).to be_empty,
15 | "#{unused_keys.leaves.count} unused i18n keys, run `i18n-tasks unused' to show them"
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/spec/models/author_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | RSpec.describe Author do
4 | it { should validate_presence_of(:name) }
5 | it { should have_many(:books) }
6 |
7 | describe "#popular_books" do
8 | it "pick only 5 books" do
9 | author = create(:author)
10 |
11 | 7.times do |index|
12 | create(:book, author: author, title: "book ##{index}")
13 | end
14 |
15 | result = author.popular_books.map(&:title)
16 |
17 | expect(result).to_not include("book #5", "book #6")
18 | expect(result).to include("book #0", "book #1",
19 | "book #2", "book #3", "book #4")
20 | end
21 | end
22 |
23 | it "sets a slug when saved" do
24 | record = create(:author, name: "Foo Bar")
25 |
26 | result = record.slug
27 |
28 | expect(result).to eq("foo-bar")
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/spec/models/book_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | RSpec.describe Book do
4 | it { should belong_to(:author) }
5 |
6 | describe "#has_summary?" do
7 | it "is true if the book has a summary" do
8 | book = build_stubbed(:book, summary: "Summary of book")
9 |
10 | result = book.has_summary?
11 |
12 | expect(result).to be(true)
13 | end
14 |
15 | it "is false if the book has a nil summary" do
16 | book = build_stubbed(:book, summary: nil)
17 |
18 | result = book.has_summary?
19 |
20 | expect(result).to be(false)
21 | end
22 |
23 | it "is false if the book's summary is all whitespace" do
24 | book = build_stubbed(:book, summary: " \t\n")
25 |
26 | result = book.has_summary?
27 |
28 | expect(result).to be(false)
29 | end
30 | end
31 |
32 | describe "#has_release_date?" do
33 | it "is true if the book has a release_date" do
34 | book = build_stubbed(:book, released_on: Time.zone.today)
35 |
36 | result = book.has_release_date?
37 |
38 | expect(result).to be(true)
39 | end
40 |
41 | it "is false if the book has no release date" do
42 | book = build_stubbed(:book, released_on: nil)
43 |
44 | result = book.has_release_date?
45 |
46 | expect(result).to be(false)
47 | end
48 | end
49 |
50 | describe "#sample_cover" do
51 | it "has a cover image" do
52 | book = build_stubbed(:book, cover: "book_cover.jpg")
53 |
54 | result = book.sample_cover
55 |
56 | expect(result).to eq("book_cover.jpg")
57 | end
58 |
59 | it "hasn't a cover image" do
60 | book = build_stubbed(:book)
61 |
62 | result = book.sample_cover
63 |
64 | expect(result).to eq("default_cover.jpg")
65 | end
66 | end
67 |
68 | describe "#sort_by" do
69 | context "when the criteria 'added'" do
70 | it "lists the newest-added first" do
71 | oldest = create(:book, released_on: 1.month.ago)
72 | newest = create(:book, released_on: 1.day.ago)
73 | middle = create(:book, released_on: 1.week.ago)
74 |
75 | result = Book.sort_by(Book::SORT_CRITERIA_RELEASE_DATE)
76 |
77 | expect(result).to eq([newest, middle, oldest])
78 | end
79 | end
80 |
81 | context "otherwise" do
82 | it "lists the books alphabetically" do
83 | book_a = create(:book, title: "Book A")
84 | book_c = create(:book, title: "Book C")
85 | book_b = create(:book, title: "Book B")
86 |
87 | result = Book.sort_by("").map(&:title)
88 |
89 | expect(result).to eq([book_a, book_b, book_c].map(&:title))
90 | end
91 |
92 | it "is case-insensitive" do
93 | book_uppercase_a = create(:book, title: "Book A")
94 | book_uppercase_c = create(:book, title: "Book C")
95 | book_lowercase_b = create(:book, title: "Book b")
96 |
97 | result = Book.sort_by("").map(&:title)
98 |
99 | expect(result).to eq(
100 | [book_uppercase_a, book_lowercase_b, book_uppercase_c].map(&:title)
101 | )
102 | end
103 | end
104 | end
105 |
106 | it "sets a slug when saved" do
107 | record = create(:book, title: "Foo Bar")
108 |
109 | result = record.slug
110 |
111 | expect(result).to eq("foo-bar")
112 | end
113 |
114 | describe "#average_rating" do
115 | context "when there are no reviews" do
116 | it "raises an exception" do
117 | book = create(:book, reviews: [])
118 |
119 | expect do
120 | book.average_rating
121 | end.to raise_error(Book::CannotCalculateAverageRatingError)
122 | end
123 | end
124 |
125 | context "with only one reviews" do
126 | it "returns the rating from that review" do
127 | review = create(:review, rating: 3)
128 | book = create(:book, reviews: [review])
129 |
130 | result = book.average_rating
131 |
132 | expect(result).to eq(3)
133 | end
134 | end
135 |
136 | context "with multiple reviews" do
137 | it "returns the average rating, ignoring reviews without a rating" do
138 | reviews = [
139 | create(:review, rating: 3),
140 | create(:review, rating: 4),
141 | create(:review, rating: nil)
142 | ]
143 | book = create(:book, reviews: reviews)
144 |
145 | result = book.average_rating
146 |
147 | expect(result).to eq(3.5)
148 | end
149 | end
150 | end
151 | end
152 |
--------------------------------------------------------------------------------
/spec/models/genre_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | RSpec.describe Genre do
4 | describe "#containing_books" do
5 | it "returns genres which have books" do
6 | fantasy_genre = create(:genre, name: "Fantasy")
7 | create(:book, genre: fantasy_genre)
8 |
9 | result = Genre.containing_books.map(&:name)
10 |
11 | expect(result).to eq(%w(Fantasy))
12 | end
13 |
14 | it "excludes genres with no books" do
15 | create(:genre, name: "Fantasy")
16 |
17 | result = Genre.containing_books
18 |
19 | expect(result).to be_empty
20 | end
21 |
22 | it "lists each genre only once" do
23 | fantasy_genre = create(:genre, name: "Fantasy")
24 | create_pair(:book, genre: fantasy_genre)
25 |
26 | result = Genre.containing_books.map(&:name)
27 |
28 | expect(result).to eq(%w(Fantasy))
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/spec/models/guest_spec.rb:
--------------------------------------------------------------------------------
1 | require_relative "../../app/models/guest"
2 |
3 | RSpec.describe Guest, "#authenticate" do
4 | it "always returns false" do
5 | authenticate_guest = Guest.new.authenticate "random_passowrd"
6 |
7 | expect(authenticate_guest).to eq false
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/spec/models/list_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | RSpec.describe List do
4 | describe ".newest_first" do
5 | it "orders by the most recenlty created first" do
6 | newest = create(:list, created_at: 1.minute.ago)
7 | oldest = create(:list, created_at: 1.day.ago)
8 | middle = create(:list, created_at: 1.hour.ago)
9 |
10 | result = List.newest_first
11 |
12 | expect(result.map(&:id)).to eq([newest.id, middle.id, oldest.id])
13 | end
14 | end
15 |
16 | describe "#highlights" do
17 | it "orders by the most recently added" do
18 | list = create(:list)
19 | newest = create(:list_entry, list: list, created_at: 1.minute.ago)
20 | oldest = create(:list_entry, list: list, created_at: 1.day.ago)
21 | middle = create(:list_entry, list: list, created_at: 1.hour.ago)
22 |
23 | result = list.highlights(maximum: 3).map(&:id)
24 |
25 | expect(result).to eq([newest.book.id, middle.book.id, oldest.book.id])
26 | end
27 |
28 | it "limits the result to the maximum requested" do
29 | books = create_list(:book, 3)
30 | list = create_list_with_books(*books)
31 |
32 | result = list.highlights(maximum: 2).map(&:id)
33 |
34 | expect(result.size).to eq(2)
35 | end
36 |
37 | def create_list_with_books(*books)
38 | create(:list).tap do |list|
39 | books.each do |book|
40 | create(:list_entry, list: list, book: book)
41 | end
42 | end
43 | end
44 | end
45 |
46 | it "sets a slug when saved" do
47 | record = create(:list, name: "Foo Bar")
48 |
49 | result = record.slug
50 |
51 | expect(result).to eq("foo-bar")
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/spec/models/review_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | RSpec.describe Review do
4 | it { is_expected.to belong_to(:book) }
5 | it { is_expected.to belong_to(:reviewer).class_name("User") }
6 | it { is_expected.to validate_presence_of(:body) }
7 | it { is_expected.to delegate_method(:email).to(:reviewer).with_prefix(true) }
8 | end
9 |
--------------------------------------------------------------------------------
/spec/models/user_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | RSpec.describe User, "validations" do
4 | it { is_expected.to validate_uniqueness_of(:email) }
5 | it { is_expected.to allow_value("validemail@domain.com").for(:email) }
6 | it { is_expected.not_to allow_value("nope@com").for(:email) }
7 | end
8 |
9 | RSpec.describe User, ".lookup_for_authentication_with" do
10 | it "retrieves a user based on the passed in identifier" do
11 | user = create :user, email: "already_registered@example.com"
12 |
13 | retrieved_user = User.lookup_for_authentication_with identifier: user.email
14 |
15 | expect(retrieved_user).to eq user
16 | end
17 |
18 | it "returns a Guest if no user is found based on the identifier" do
19 | non_existent_email = "not_here@example.com"
20 |
21 | retrieved_user =
22 | User.lookup_for_authentication_with identifier: non_existent_email
23 |
24 | expect(retrieved_user).to be_a Guest
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/spec/policies/review_policy_spec.rb:
--------------------------------------------------------------------------------
1 | require_relative "../../app/policies/review_policy"
2 |
3 | RSpec.describe ReviewPolicy, "#authorized_for_review?" do
4 | it "returns true if the user has not written a review for the book yet" do
5 | reviewer_to_auth = instance_double("Reviewer", present?: true)
6 | reviewers = []
7 |
8 | review_authorization = ReviewPolicy.new(
9 | reviewers: reviewers,
10 | reviewer_to_auth: reviewer_to_auth
11 | ).authorized_for_review?
12 |
13 | expect(review_authorization).to eq true
14 | end
15 |
16 | it "returns false if the user has written a review for the book already" do
17 | reviewer_to_auth = instance_double("Reviewer", present?: true)
18 | reviewers = [reviewer_to_auth]
19 |
20 | review_authorization = ReviewPolicy.new(
21 | reviewers: reviewers,
22 | reviewer_to_auth: reviewer_to_auth
23 | ).authorized_for_review?
24 |
25 | expect(review_authorization).to eq false
26 | end
27 |
28 | it "returns false if the user is not signed in" do
29 | reviewer_to_auth = instance_double("Reviewer", present?: false)
30 | reviewers = [reviewer_to_auth]
31 |
32 | review_authorization = ReviewPolicy.new(
33 | reviewers: reviewers,
34 | reviewer_to_auth: reviewer_to_auth
35 | ).authorized_for_review?
36 |
37 | expect(review_authorization).to eq false
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/spec/rails_helper.rb:
--------------------------------------------------------------------------------
1 | ENV["RAILS_ENV"] = "test"
2 |
3 | require File.expand_path("../../config/environment", __FILE__)
4 | abort("DATABASE_URL environment variable is set") if ENV["DATABASE_URL"]
5 |
6 | require "rspec/rails"
7 |
8 | Dir[Rails.root.join("spec/support/**/*.rb")].sort.each { |file| require file }
9 |
10 | module Features
11 | # Extend this module in spec/support/features/*.rb
12 | include Formulaic::Dsl
13 | include LoginHelpers
14 | include BookHelpers
15 | end
16 |
17 | RSpec.configure do |config|
18 | config.before(:each, js: true) do
19 | page.driver.block_unknown_urls
20 | end
21 | config.include Features, type: :feature
22 | config.infer_base_class_for_anonymous_controllers = false
23 | config.infer_spec_type_from_file_location!
24 | config.use_transactional_fixtures = false
25 | end
26 |
27 | ActiveRecord::Migration.maintain_test_schema!
28 | Capybara.javascript_driver = :webkit
29 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | if ENV.fetch("COVERAGE", false)
2 | require "simplecov"
3 | SimpleCov.start "rails"
4 | end
5 |
6 | require "webmock/rspec"
7 |
8 | # http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
9 | RSpec.configure do |config|
10 | config.expect_with :rspec do |expectations|
11 | expectations.syntax = :expect
12 | end
13 |
14 | config.mock_with :rspec do |mocks|
15 | mocks.syntax = :expect
16 | mocks.verify_partial_doubles = true
17 | end
18 |
19 | config.example_status_persistence_file_path = "tmp/rspec_examples.txt"
20 | config.order = :random
21 | end
22 |
23 | WebMock.disable_net_connect!(allow_localhost: true)
24 |
--------------------------------------------------------------------------------
/spec/support/action_mailer.rb:
--------------------------------------------------------------------------------
1 | RSpec.configure do |config|
2 | config.before(:each) do
3 | ActionMailer::Base.deliveries.clear
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/support/database_cleaner.rb:
--------------------------------------------------------------------------------
1 | RSpec.configure do |config|
2 | config.before(:suite) do
3 | DatabaseCleaner.clean_with(:deletion)
4 | end
5 |
6 | config.before(:each) do
7 | DatabaseCleaner.strategy = :transaction
8 | end
9 |
10 | config.before(:each, js: true) do
11 | DatabaseCleaner.strategy = :deletion
12 | end
13 |
14 | config.before(:each) do
15 | DatabaseCleaner.start
16 | end
17 |
18 | config.after(:each) do
19 | DatabaseCleaner.clean
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/spec/support/factory_girl.rb:
--------------------------------------------------------------------------------
1 | RSpec.configure do |config|
2 | config.include FactoryGirl::Syntax::Methods
3 | end
4 |
--------------------------------------------------------------------------------
/spec/support/features/book_helpers.rb:
--------------------------------------------------------------------------------
1 | module BookHelpers
2 | def have_book_title(title)
3 | have_css(".book h1", text: title)
4 | end
5 |
6 | def have_book_cover(cover)
7 | have_css("img[src*='#{cover}']")
8 | end
9 |
10 | def have_book_summary(summary)
11 | have_css(".book .summary", text: summary)
12 | end
13 |
14 | def have_displayed_books(count)
15 | have_css(".book", count: count)
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/spec/support/features/login_helpers.rb:
--------------------------------------------------------------------------------
1 | module LoginHelpers
2 | def login_as(user)
3 | visit root_path
4 | click_link t("application.header.login")
5 | fill_in t("registration.email"), with: user.email
6 | fill_in t("registration.password"), with: user.password
7 | click_button t("registration.login")
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/spec/support/i18n.rb:
--------------------------------------------------------------------------------
1 | RSpec.configure do |config|
2 | config.include AbstractController::Translation
3 | end
4 |
--------------------------------------------------------------------------------
/spec/support/matchers/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taylorrf/rubybookshelf/8b94ee49cb6ebec4fcf567bb66298604c07dddfe/spec/support/matchers/.keep
--------------------------------------------------------------------------------
/spec/support/mixins/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taylorrf/rubybookshelf/8b94ee49cb6ebec4fcf567bb66298604c07dddfe/spec/support/mixins/.keep
--------------------------------------------------------------------------------
/spec/support/shared_examples/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taylorrf/rubybookshelf/8b94ee49cb6ebec4fcf567bb66298604c07dddfe/spec/support/shared_examples/.keep
--------------------------------------------------------------------------------
/spec/support/shoulda_matchers.rb:
--------------------------------------------------------------------------------
1 | Shoulda::Matchers.configure do |config|
2 | config.integrate do |with|
3 | with.test_framework :rspec
4 | with.library :rails
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/spec/views/authors/index.html.erb_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | RSpec.describe "authors/index" do
4 | context "when there are authors to list" do
5 | it "lists the authors" do
6 | martin_fowler = build_stubbed(:author, name: "Martin Fowler")
7 | uncle_bob = build_stubbed(:author, name: "Uncle Bob")
8 |
9 | render template: "authors/index",
10 | locals: { authors: [martin_fowler, uncle_bob] }
11 |
12 | expect(rendered).to have_css("h2", text: "Martin Fowler")
13 | expect(rendered).to have_css("h2", text: "Uncle Bob")
14 | end
15 |
16 | it "lists the popular books by each author" do
17 | uncle_bob = build_stubbed(:author, name: "Uncle Bob")
18 | clean_code = build_stubbed(:book,
19 | cover: "http://amz.on/clean_code.jpg")
20 | clean_coders = build_stubbed(:book,
21 | cover: "http://amz.on/clean_coders.jpg")
22 | allow(uncle_bob).to receive(:popular_books).
23 | and_return([clean_code, clean_coders])
24 |
25 | render template: "authors/index", locals: { authors: [uncle_bob] }
26 |
27 | expect(rendered).to have_css("img[src*='http://amz.on/clean_code.jpg']")
28 | expect(rendered).to have_css("img[src*='http://amz.on/clean_coders.jpg']")
29 | end
30 | end
31 |
32 | context "when there are no authors to list" do
33 | it "displays an appropriate notice" do
34 | render template: "authors/index", locals: { authors: [] }
35 |
36 | expect(rendered).to have_text(t("authors.index.no_authors_found"))
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/spec/views/authors/show.html.erb_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | RSpec.describe "authors/show" do
4 | context "when the author has books" do
5 | it "displays their books" do
6 | author = build_stubbed(:author, name: "Aaron")
7 | book = build_stubbed(:book, author: author, title: "Legendary Ruby")
8 | allow(author).to receive(:books).and_return([book])
9 |
10 | render template: "authors/show", locals: { author: author }
11 |
12 | expect(rendered).to have_css("a", text: "Legendary Ruby")
13 | end
14 | end
15 |
16 | context "when the author hasn't books" do
17 | it "only displays their basic info" do
18 | author = build_stubbed(:author, name: "Aaron", description: "Tender")
19 |
20 | render template: "authors/show", locals: { author: author }
21 |
22 | expect(rendered).to have_css("h1", text: "Aaron")
23 | expect(rendered).to have_css("p", text: "Tender")
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/spec/views/books/_details.html.erb_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | RSpec.describe "books/_details" do
4 | context "when the book has a summary" do
5 | it "displays the summary" do
6 | book = build_stubbed(
7 | :book, summary: "The first book in the series\nProbably the best"
8 | )
9 | render template: "books/_details", locals: { book: book }
10 |
11 | expect(rendered).to have_css("p.summary",
12 | text: "The first book in the series")
13 | expect(rendered).to have_css("p.summary",
14 | text: "Probably the best")
15 | end
16 | end
17 |
18 | context "when the book has no summary" do
19 | it "doesn't display a summary" do
20 | book = build_stubbed(:book)
21 |
22 | render template: "books/_details", locals: { book: book }
23 |
24 | expect(rendered).to_not have_css("p.summary")
25 | end
26 | end
27 |
28 | context "when the book has a cover sample" do
29 | it "displays the cover sample" do
30 | book = build_stubbed(:book, cover: "book_cover.jpg")
31 |
32 | render template: "books/_details", locals: { book: book }
33 |
34 | expect(rendered).to have_cover("book_cover.jpg")
35 | end
36 | end
37 |
38 | context "when the book has no a cover sample" do
39 | it "doesn't display the cover sample" do
40 | book = build_stubbed(:book)
41 |
42 | render template: "books/_details", locals: { book: book }
43 |
44 | expect(rendered).to have_cover("default_cover.jpg")
45 | end
46 | end
47 |
48 | context "when the book has an author" do
49 | it "displays author name" do
50 | author = build_stubbed(:author, name: "John")
51 | book = build_stubbed(:book, author: author)
52 |
53 | render template: "books/_details", locals: { book: book }
54 |
55 | expect(rendered).to have_css("h3", "John")
56 | end
57 | end
58 |
59 | context "when the book has no an author" do
60 | it "doesn't display author name" do
61 | book = build_stubbed(:book)
62 |
63 | render template: "books/_details", locals: { book: book }
64 |
65 | expect(rendered).to_not have_css("h3", "John")
66 | end
67 | end
68 |
69 | context "when the book has a release date" do
70 | it "displays the release date" do
71 | release_date = Date.new(2015, 4, 13)
72 | book = build_stubbed(:book, released_on: release_date)
73 |
74 | render template: "books/_details", locals: { book: book }
75 |
76 | expect(rendered).to have_css(".released-on", text: "April 13, 2015")
77 | end
78 | end
79 |
80 | context "when the book has no release date" do
81 | it "doesn't display the release date" do
82 | book = build_stubbed(:book, released_on: nil)
83 |
84 | render template: "books/_details", locals: { book: book }
85 |
86 | expect(rendered).to_not have_css(".released-on")
87 | end
88 | end
89 |
90 | context "when the book has an average rating" do
91 | it "displays the average rating" do
92 | book = build_stubbed(:book)
93 | allow(book).to receive(:has_average_rating?).and_return(true)
94 | allow(book).to receive(:average_rating).and_return(3)
95 |
96 | render template: "books/_details", locals: { book: book }
97 |
98 | expect(rendered).to have_css(".average-rating", text: 3)
99 | end
100 | end
101 |
102 | context "when the book has no average rating" do
103 | it "displays no rating" do
104 | book = build_stubbed(:book)
105 | allow(book).to receive(:has_average_rating?).and_return(false)
106 |
107 | render template: "books/_details", locals: { book: book }
108 |
109 | expect(rendered).to_not have_css(".average-rating")
110 | end
111 | end
112 |
113 | def have_cover(image)
114 | have_css("img[src*='#{image}']")
115 | end
116 | end
117 |
--------------------------------------------------------------------------------
/spec/views/books/index.html.erb_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | RSpec.describe "books/index" do
4 | context "when there are books to list" do
5 | it "lists the books" do
6 | allow(view).to receive(:sort_path)
7 | books = [
8 | build_stubbed(:book, title: "The Fellowship of the Ring"),
9 | build_stubbed(:book, title: "The Two Towers")
10 | ]
11 |
12 | filter = Filter.new
13 | render template: "books/index", locals: {
14 | books: Kaminari.paginate_array(books).page,
15 | filter: filter
16 | }
17 |
18 | within("ul.books") do
19 | expect(rendered).to have_css("li", text: "The Fellowship of the Ring")
20 | expect(rendered).to have_css("li", text: "The Two Towers")
21 | end
22 | end
23 | end
24 |
25 | context "when there are no books to display" do
26 | it "displays an appropriate notice" do
27 | books = []
28 |
29 | render template: "books/index", locals: {
30 | books: Kaminari.paginate_array(books).page,
31 | filter: Filter.new
32 | }
33 |
34 | expect(rendered).to have_text(t("books.index.no_books_yet"))
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/spec/views/lists/_list.html.erb_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | RSpec.describe "lists/_list" do
4 | context "when the list is empty" do
5 | it "shows an appropriate message" do
6 | list = build_stubbed(:list)
7 |
8 | render template: "lists/_list", locals: { list: list }
9 |
10 | expect(rendered).to have_css("p", text: t(".list.empty"))
11 | end
12 | end
13 |
14 | context "when there are books to list" do
15 | it "lists the books" do
16 | list = build_stubbed(:list)
17 | allow(list).to receive(:empty?).and_return(false)
18 | allow(list).to receive(:highlights).and_return([
19 | build_stubbed(:book),
20 | build_stubbed(:book)
21 | ])
22 |
23 | render template: "lists/_list", locals: { list: list }
24 |
25 | expect(rendered).to have_css("ul.books li")
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/spec/views/lists/index.html.erb_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | RSpec.describe "lists/index" do
4 | context "when there are lists" do
5 | it "displays the lists" do
6 | lists = [build_stubbed(:list)]
7 |
8 | render template: "lists/index", locals: { lists: lists }
9 |
10 | expect(rendered).to have_css("ul.lists h2")
11 | end
12 | end
13 |
14 | context "when there are no lists to display" do
15 | it "displays an appropriate notice" do
16 | render template: "lists/index", locals: { lists: [] }
17 |
18 | expect(rendered).to have_text(t(".index.no_lists_yet"))
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/spec/views/lists/show.html.erb_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | RSpec.describe "lists/show" do
4 | context "when the list is empty" do
5 | it "shows an appropriate message" do
6 | list = build_stubbed(:list)
7 |
8 | render template: "lists/show", locals: { list: list }
9 |
10 | expect(rendered).to have_text(t(".show.empty"))
11 | end
12 | end
13 |
14 | context "when there are books to list" do
15 | it "lists the books" do
16 | list = build_stubbed(:list)
17 | allow(list).to receive(:empty?).and_return(false)
18 | allow(list).to receive(:books).and_return([
19 | build_stubbed(:book),
20 | build_stubbed(:book)
21 | ])
22 |
23 | render template: "lists/show", locals: { list: list }
24 |
25 | expect(rendered).to have_css(".book", count: 2)
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/spec/views/reviews/_review.html.erb_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | RSpec.describe "books/_review" do
4 | it "displays the review body" do
5 | review = build_stubbed(:review, rating: 4, body: "Great book")
6 | allow(review).to receive(:reviewer_email)
7 |
8 | render template: "reviews/_review", locals: { review: review }
9 |
10 | expect(rendered).to have_text("Great book")
11 | end
12 |
13 | context "with a rating" do
14 | it "displays the rating" do
15 | review = build_stubbed(:review, rating: 4)
16 | allow(review).to receive(:reviewer_email)
17 |
18 | render template: "reviews/_review", locals: { review: review }
19 |
20 | expect(rendered).to have_rating(4)
21 | end
22 | end
23 |
24 | context "without a rating" do
25 | it "displays the rating" do
26 | review = build_stubbed(:review, rating: nil)
27 | allow(review).to receive(:reviewer_email)
28 |
29 | render template: "reviews/_review", locals: { review: review }
30 |
31 | expect(rendered).to have_no_rating
32 | end
33 | end
34 |
35 | def have_rating(rating)
36 | have_css(".rating", text: rating)
37 | end
38 |
39 | def have_no_rating
40 | have_css(".rating", count: 0)
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
Reviews
3 |