├── .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 | [![Build Status](https://circleci.com/gh/taylorrf/rubybookshelf.svg?style=svg)](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 | 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 |
2 |

Reviews

3 |
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 | 11 | <% end %> 12 |
  • 13 | -------------------------------------------------------------------------------- /app/views/lists/index.html.erb: -------------------------------------------------------------------------------- 1 |

    <%= t(".header")%>

    2 | 3 | <% if lists.any? %> 4 | 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 |
    2 |
    3 | <%= gravatar_image_tag(review.reviewer_email, :alt => review.reviewer_email) %> 4 |
    5 |
    6 |

    <%= review.reviewer_email %>

    7 | <%= simple_format review.body %> 8 | <% if review.rating %> 9 |
    Rating: <%= review.rating %>
    10 | <% end %> 11 | <%= time_ago_in_words review.created_at %> ago 12 |
    13 |
    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 | --------------------------------------------------------------------------------