From f4356211aa3a153e29ee872f7516770091154116 Mon Sep 17 00:00:00 2001 From: Sarah Duve Date: Mon, 22 Jul 2024 21:31:51 -0400 Subject: [PATCH] Add NOTES --- .gitignore | 3 + Gemfile | 5 +- Gemfile.lock | 55 ++++++++++++------- NOTES.md | 12 ++++ Procfile.dev | 2 + app/assets/builds/.keep | 0 app/assets/config/manifest.js | 1 + app/controllers/stories_controller.rb | 14 +++++ app/helpers/stories_helper.rb | 2 + app/models/story.rb | 4 ++ app/models/user.rb | 1 + app/models/user_story.rb | 4 ++ app/services/fetch_top_stories_service.rb | 26 +++++++++ app/views/devise/confirmations/new.html.erb | 16 ++++++ .../mailer/confirmation_instructions.html.erb | 5 ++ .../devise/mailer/email_changed.html.erb | 7 +++ .../devise/mailer/password_change.html.erb | 3 + .../reset_password_instructions.html.erb | 8 +++ .../mailer/unlock_instructions.html.erb | 7 +++ app/views/devise/passwords/edit.html.erb | 25 +++++++++ app/views/devise/passwords/new.html.erb | 16 ++++++ app/views/devise/registrations/edit.html.erb | 53 ++++++++++++++++++ app/views/devise/registrations/new.html.erb | 39 +++++++++++++ app/views/devise/sessions/new.html.erb | 26 +++++++++ .../devise/shared/_error_messages.html.erb | 15 +++++ app/views/devise/shared/_links.html.erb | 25 +++++++++ app/views/devise/unlocks/new.html.erb | 16 ++++++ app/views/stories/index.html.erb | 21 +++++++ bin/dev | 16 ++++++ config/environments/development.rb | 7 +++ config/environments/test.rb | 2 +- config/routes.rb | 6 +- db/migrate/20240722051921_create_stories.rb | 11 ++++ .../20240722052034_create_user_stories.rb | 10 ++++ db/schema.rb | 21 ++++++- spec/factories/stories.rb | 8 +++ spec/factories/user_stories.rb | 6 ++ spec/factories/users.rb | 8 +++ spec/models/story_spec.rb | 5 ++ spec/models/user_story_spec.rb | 5 ++ spec/rails_helper.rb | 24 ++++++-- spec/requests/stories_spec.rb | 22 ++++++++ spec/spec_helper.rb | 6 +- spec/views/stories/index.html.erb_spec.rb | 19 +++++++ 44 files changed, 554 insertions(+), 33 deletions(-) create mode 100644 NOTES.md create mode 100644 Procfile.dev create mode 100644 app/assets/builds/.keep create mode 100644 app/controllers/stories_controller.rb create mode 100644 app/helpers/stories_helper.rb create mode 100644 app/models/story.rb create mode 100644 app/models/user_story.rb create mode 100644 app/services/fetch_top_stories_service.rb create mode 100644 app/views/devise/confirmations/new.html.erb create mode 100644 app/views/devise/mailer/confirmation_instructions.html.erb create mode 100644 app/views/devise/mailer/email_changed.html.erb create mode 100644 app/views/devise/mailer/password_change.html.erb create mode 100644 app/views/devise/mailer/reset_password_instructions.html.erb create mode 100644 app/views/devise/mailer/unlock_instructions.html.erb create mode 100644 app/views/devise/passwords/edit.html.erb create mode 100644 app/views/devise/passwords/new.html.erb create mode 100644 app/views/devise/registrations/edit.html.erb create mode 100644 app/views/devise/registrations/new.html.erb create mode 100644 app/views/devise/sessions/new.html.erb create mode 100644 app/views/devise/shared/_error_messages.html.erb create mode 100644 app/views/devise/shared/_links.html.erb create mode 100644 app/views/devise/unlocks/new.html.erb create mode 100644 app/views/stories/index.html.erb create mode 100755 bin/dev create mode 100644 db/migrate/20240722051921_create_stories.rb create mode 100644 db/migrate/20240722052034_create_user_stories.rb create mode 100644 spec/factories/stories.rb create mode 100644 spec/factories/user_stories.rb create mode 100644 spec/factories/users.rb create mode 100644 spec/models/story_spec.rb create mode 100644 spec/models/user_story_spec.rb create mode 100644 spec/requests/stories_spec.rb create mode 100644 spec/views/stories/index.html.erb_spec.rb diff --git a/.gitignore b/.gitignore index 82701fed..30ea7d8c 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,6 @@ /yarn-error.log .byebug_history + +/app/assets/builds/* +!/app/assets/builds/.keep diff --git a/Gemfile b/Gemfile index 5a8ffc43..bfdf2ae2 100644 --- a/Gemfile +++ b/Gemfile @@ -12,7 +12,9 @@ gem 'pg' gem 'pry-rails' gem 'puma' gem 'rails', '~> 7.0.3' -gem 'rspec-rails' +gem 'rspec-rails', group: [:development, :test] +gem 'factory_bot_rails', group: [:development, :test] +gem 'faker', group: [:development, :test] gem 'sass-rails' gem 'selenium-webdriver', group: [:development, :test] gem 'spring', group: :development @@ -20,3 +22,4 @@ gem 'turbolinks' gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] gem 'uglifier' gem 'web-console', group: :development +gem 'bullet' diff --git a/Gemfile.lock b/Gemfile.lock index 14ec6457..a380e658 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -66,22 +66,25 @@ GEM i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) - addressable (2.8.1) - public_suffix (>= 2.0.2, < 6.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + base64 (0.2.0) bcrypt (3.1.18) bindex (0.8.1) builder (3.2.4) + bullet (7.2.0) + activesupport (>= 3.0.0) + uniform_notifier (~> 1.11) byebug (11.1.3) - capybara (3.37.1) + capybara (3.40.0) addressable matrix mini_mime (>= 0.1.3) - nokogiri (~> 1.8) + nokogiri (~> 1.11) rack (>= 1.6.0) rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) - childprocess (4.1.0) coderay (1.1.3) coffee-rails (5.0.0) coffee-script (>= 2.2.0) @@ -102,6 +105,13 @@ GEM digest (3.1.0) erubi (1.11.0) execjs (2.8.1) + factory_bot (6.4.6) + activesupport (>= 5.0.0) + factory_bot_rails (6.4.3) + factory_bot (~> 6.4) + railties (>= 5.0.0) + faker (3.4.2) + i18n (>= 1.8.11, < 2) ffi (1.15.5) globalid (1.0.0) activesupport (>= 5.0) @@ -113,6 +123,7 @@ GEM listen (3.7.1) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) + logger (1.6.0) loofah (2.19.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) @@ -121,8 +132,8 @@ GEM marcel (1.0.2) matrix (0.4.2) method_source (1.0.0) - mini_mime (1.1.2) - mini_portile2 (2.8.0) + mini_mime (1.1.5) + mini_portile2 (2.8.7) minitest (5.16.3) net-imap (0.2.3) digest @@ -139,8 +150,8 @@ GEM net-protocol timeout nio4r (2.5.8) - nokogiri (1.13.8) - mini_portile2 (~> 2.8.0) + nokogiri (1.16.6) + mini_portile2 (~> 2.8.2) racc (~> 1.4) orm_adapter (0.5.0) pg (1.4.3) @@ -149,12 +160,12 @@ GEM method_source (~> 1.0) pry-rails (0.3.9) pry (>= 0.10.4) - public_suffix (5.0.0) + public_suffix (6.0.0) puma (5.6.5) nio4r (~> 2.0) - racc (1.6.0) - rack (2.2.4) - rack-test (2.0.2) + racc (1.8.0) + rack (2.2.9) + rack-test (2.1.0) rack (>= 1.3) rails (7.0.4) actioncable (= 7.0.4) @@ -186,11 +197,12 @@ GEM rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) - regexp_parser (2.5.0) + regexp_parser (2.9.2) responders (3.0.1) actionpack (>= 5.0) railties (>= 5.0) - rexml (3.2.5) + rexml (3.3.2) + strscan rspec-core (3.11.0) rspec-support (~> 3.11.0) rspec-expectations (3.11.1) @@ -219,8 +231,9 @@ GEM sprockets (> 3.0) sprockets-rails tilt - selenium-webdriver (4.4.0) - childprocess (>= 0.5, < 5.0) + selenium-webdriver (4.23.0) + base64 (~> 0.2) + logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) @@ -232,7 +245,7 @@ GEM actionpack (>= 5.2) activesupport (>= 5.2) sprockets (>= 3.0.0) - strscan (3.0.4) + strscan (3.1.0) thor (1.2.1) tilt (2.0.11) timeout (0.3.0) @@ -243,6 +256,7 @@ GEM concurrent-ruby (~> 1.0) uglifier (4.2.0) execjs (>= 0.3.0, < 3) + uniform_notifier (1.16.0) warden (1.2.9) rack (>= 2.0.9) web-console (4.2.0) @@ -250,7 +264,7 @@ GEM activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) - websocket (1.2.9) + websocket (1.2.11) websocket-driver (0.7.5) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) @@ -262,10 +276,13 @@ PLATFORMS ruby DEPENDENCIES + bullet byebug capybara coffee-rails devise + factory_bot_rails + faker jbuilder listen pg diff --git a/NOTES.md b/NOTES.md new file mode 100644 index 00000000..cb031cd8 --- /dev/null +++ b/NOTES.md @@ -0,0 +1,12 @@ +## Notes + +To test: `bundle exec rspec` + +TODO: +- [ ] Add model and system tests +- [ ] Add error handling +- [ ] Decide on ordering of "Interesting Stories" and how far back to display +- [ ] Explore other caching strategies + + + diff --git a/Procfile.dev b/Procfile.dev new file mode 100644 index 00000000..da151fee --- /dev/null +++ b/Procfile.dev @@ -0,0 +1,2 @@ +web: bin/rails server +css: bin/rails tailwindcss:watch diff --git a/app/assets/builds/.keep b/app/assets/builds/.keep new file mode 100644 index 00000000..e69de29b diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js index b16e53d6..d333340b 100644 --- a/app/assets/config/manifest.js +++ b/app/assets/config/manifest.js @@ -1,3 +1,4 @@ //= link_tree ../images //= link_directory ../javascripts .js //= link_directory ../stylesheets .css +//= link_tree ../builds diff --git a/app/controllers/stories_controller.rb b/app/controllers/stories_controller.rb new file mode 100644 index 00000000..7c6897ed --- /dev/null +++ b/app/controllers/stories_controller.rb @@ -0,0 +1,14 @@ +class StoriesController < ApplicationController + before_action :authenticate_user! + + def index + @stories = FetchTopStoriesService.call + @interesting_stories = Story.joins(:user_stories).distinct.includes([:users]) + end + + def mark_as_interesting + story = Story.find_or_create_by(hn_id: params[:hn_id], title: params[:title], url: params[:url]) + current_user.user_stories.find_or_create_by(story: story) + redirect_to root_path, notice: 'Story marked as interesting.' + end +end diff --git a/app/helpers/stories_helper.rb b/app/helpers/stories_helper.rb new file mode 100644 index 00000000..43e5cd8f --- /dev/null +++ b/app/helpers/stories_helper.rb @@ -0,0 +1,2 @@ +module StoriesHelper +end diff --git a/app/models/story.rb b/app/models/story.rb new file mode 100644 index 00000000..62f87583 --- /dev/null +++ b/app/models/story.rb @@ -0,0 +1,4 @@ +class Story < ApplicationRecord + has_many :user_stories + has_many :users, through: :user_stories +end diff --git a/app/models/user.rb b/app/models/user.rb index b2091f9a..1aa41f82 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,4 +1,5 @@ class User < ApplicationRecord + has_many :user_stories # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable and :omniauthable devise :database_authenticatable, :registerable, diff --git a/app/models/user_story.rb b/app/models/user_story.rb new file mode 100644 index 00000000..b54aef28 --- /dev/null +++ b/app/models/user_story.rb @@ -0,0 +1,4 @@ +class UserStory < ApplicationRecord + belongs_to :user + belongs_to :story +end diff --git a/app/services/fetch_top_stories_service.rb b/app/services/fetch_top_stories_service.rb new file mode 100644 index 00000000..ff193a4d --- /dev/null +++ b/app/services/fetch_top_stories_service.rb @@ -0,0 +1,26 @@ +class FetchTopStoriesService + require 'net/http' + require 'json' + + TOP_STORIES_URL = 'https://hacker-news.firebaseio.com/v0/topstories.json' + STORY_URL = 'https://hacker-news.firebaseio.com/v0/item/' + + def self.call + story_ids = fetch_top_story_ids.first(30) + story_ids.map { |id| fetch_story_details(id) } + end + + def self.fetch_top_story_ids + response = Net::HTTP.get(URI(TOP_STORIES_URL)) + JSON.parse(response) + end + + def self.fetch_story_details(story_id) + Rails.cache.fetch("story_#{story_id}", expires_in: 1.day) do + Rails.logger.info "Cache miss for story_id: #{story_id}. Fetching from API." + response = Net::HTTP.get(URI("#{STORY_URL}#{story_id}.json")) + JSON.parse(response) + end + end +end + diff --git a/app/views/devise/confirmations/new.html.erb b/app/views/devise/confirmations/new.html.erb new file mode 100644 index 00000000..b12dd0cb --- /dev/null +++ b/app/views/devise/confirmations/new.html.erb @@ -0,0 +1,16 @@ +

Resend confirmation instructions

+ +<%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email", value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email) %> +
+ +
+ <%= f.submit "Resend confirmation instructions" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/devise/mailer/confirmation_instructions.html.erb b/app/views/devise/mailer/confirmation_instructions.html.erb new file mode 100644 index 00000000..dc55f64f --- /dev/null +++ b/app/views/devise/mailer/confirmation_instructions.html.erb @@ -0,0 +1,5 @@ +

Welcome <%= @email %>!

+ +

You can confirm your account email through the link below:

+ +

<%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %>

diff --git a/app/views/devise/mailer/email_changed.html.erb b/app/views/devise/mailer/email_changed.html.erb new file mode 100644 index 00000000..32f4ba80 --- /dev/null +++ b/app/views/devise/mailer/email_changed.html.erb @@ -0,0 +1,7 @@ +

Hello <%= @email %>!

+ +<% if @resource.try(:unconfirmed_email?) %> +

We're contacting you to notify you that your email is being changed to <%= @resource.unconfirmed_email %>.

+<% else %> +

We're contacting you to notify you that your email has been changed to <%= @resource.email %>.

+<% end %> diff --git a/app/views/devise/mailer/password_change.html.erb b/app/views/devise/mailer/password_change.html.erb new file mode 100644 index 00000000..b41daf47 --- /dev/null +++ b/app/views/devise/mailer/password_change.html.erb @@ -0,0 +1,3 @@ +

Hello <%= @resource.email %>!

+ +

We're contacting you to notify you that your password has been changed.

diff --git a/app/views/devise/mailer/reset_password_instructions.html.erb b/app/views/devise/mailer/reset_password_instructions.html.erb new file mode 100644 index 00000000..f667dc12 --- /dev/null +++ b/app/views/devise/mailer/reset_password_instructions.html.erb @@ -0,0 +1,8 @@ +

Hello <%= @resource.email %>!

+ +

Someone has requested a link to change your password. You can do this through the link below.

+ +

<%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %>

+ +

If you didn't request this, please ignore this email.

+

Your password won't change until you access the link above and create a new one.

diff --git a/app/views/devise/mailer/unlock_instructions.html.erb b/app/views/devise/mailer/unlock_instructions.html.erb new file mode 100644 index 00000000..41e148bf --- /dev/null +++ b/app/views/devise/mailer/unlock_instructions.html.erb @@ -0,0 +1,7 @@ +

Hello <%= @resource.email %>!

+ +

Your account has been locked due to an excessive number of unsuccessful sign in attempts.

+ +

Click the link below to unlock your account:

+ +

<%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %>

diff --git a/app/views/devise/passwords/edit.html.erb b/app/views/devise/passwords/edit.html.erb new file mode 100644 index 00000000..5fbb9ff0 --- /dev/null +++ b/app/views/devise/passwords/edit.html.erb @@ -0,0 +1,25 @@ +

Change your password

+ +<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + <%= f.hidden_field :reset_password_token %> + +
+ <%= f.label :password, "New password" %>
+ <% if @minimum_password_length %> + (<%= @minimum_password_length %> characters minimum)
+ <% end %> + <%= f.password_field :password, autofocus: true, autocomplete: "new-password" %> +
+ +
+ <%= f.label :password_confirmation, "Confirm new password" %>
+ <%= f.password_field :password_confirmation, autocomplete: "new-password" %> +
+ +
+ <%= f.submit "Change my password" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/devise/passwords/new.html.erb b/app/views/devise/passwords/new.html.erb new file mode 100644 index 00000000..9b486b81 --- /dev/null +++ b/app/views/devise/passwords/new.html.erb @@ -0,0 +1,16 @@ +

Forgot your password?

+ +<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %> +
+ +
+ <%= f.submit "Send me reset password instructions" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb new file mode 100644 index 00000000..90acb295 --- /dev/null +++ b/app/views/devise/registrations/edit.html.erb @@ -0,0 +1,53 @@ +

Edit <%= resource_name.to_s.humanize %>

+ +<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :first_name %> + <%= f.text_field :first_name %> +
+ +
+ <%= f.label :last_name %> + <%= f.text_field :last_name %> +
+ +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %> +
+ + <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %> +
Currently waiting confirmation for: <%= resource.unconfirmed_email %>
+ <% end %> + +
+ <%= f.label :password %> (leave blank if you don't want to change it)
+ <%= f.password_field :password, autocomplete: "new-password" %> + <% if @minimum_password_length %> +
+ <%= @minimum_password_length %> characters minimum + <% end %> +
+ +
+ <%= f.label :password_confirmation %>
+ <%= f.password_field :password_confirmation, autocomplete: "new-password" %> +
+ +
+ <%= f.label :current_password %> (we need your current password to confirm your changes)
+ <%= f.password_field :current_password, autocomplete: "current-password" %> +
+ +
+ <%= f.submit "Update" %> +
+<% end %> + +

Cancel my account

+ +

Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %>

+ +<%= link_to "Back", :back %> diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb new file mode 100644 index 00000000..6561320e --- /dev/null +++ b/app/views/devise/registrations/new.html.erb @@ -0,0 +1,39 @@ +

Sign up

+ +<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :first_name %> + <%= f.text_field :first_name, autofocus: true %> +
+ +
+ <%= f.label :last_name %> + <%= f.text_field :last_name %> +
+ +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %> +
+ +
+ <%= f.label :password %> + <% if @minimum_password_length %> + (<%= @minimum_password_length %> characters minimum) + <% end %>
+ <%= f.password_field :password, autocomplete: "new-password" %> +
+ +
+ <%= f.label :password_confirmation %>
+ <%= f.password_field :password_confirmation, autocomplete: "new-password" %> +
+ +
+ <%= f.submit "Sign up" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb new file mode 100644 index 00000000..5ede9648 --- /dev/null +++ b/app/views/devise/sessions/new.html.erb @@ -0,0 +1,26 @@ +

Log in

+ +<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %> +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %> +
+ +
+ <%= f.label :password %>
+ <%= f.password_field :password, autocomplete: "current-password" %> +
+ + <% if devise_mapping.rememberable? %> +
+ <%= f.check_box :remember_me %> + <%= f.label :remember_me %> +
+ <% end %> + +
+ <%= f.submit "Log in" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/devise/shared/_error_messages.html.erb b/app/views/devise/shared/_error_messages.html.erb new file mode 100644 index 00000000..ba7ab887 --- /dev/null +++ b/app/views/devise/shared/_error_messages.html.erb @@ -0,0 +1,15 @@ +<% if resource.errors.any? %> +
+

+ <%= I18n.t("errors.messages.not_saved", + count: resource.errors.count, + resource: resource.class.model_name.human.downcase) + %> +

+ +
+<% end %> diff --git a/app/views/devise/shared/_links.html.erb b/app/views/devise/shared/_links.html.erb new file mode 100644 index 00000000..96a94124 --- /dev/null +++ b/app/views/devise/shared/_links.html.erb @@ -0,0 +1,25 @@ +<%- if controller_name != 'sessions' %> + <%= link_to "Log in", new_session_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.registerable? && controller_name != 'registrations' %> + <%= link_to "Sign up", new_registration_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> + <%= link_to "Forgot your password?", new_password_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> + <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> + <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.omniauthable? %> + <%- resource_class.omniauth_providers.each do |provider| %> + <%= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), method: :post %>
+ <% end %> +<% end %> diff --git a/app/views/devise/unlocks/new.html.erb b/app/views/devise/unlocks/new.html.erb new file mode 100644 index 00000000..ffc34de8 --- /dev/null +++ b/app/views/devise/unlocks/new.html.erb @@ -0,0 +1,16 @@ +

Resend unlock instructions

+ +<%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %> +
+ +
+ <%= f.submit "Resend unlock instructions" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/stories/index.html.erb b/app/views/stories/index.html.erb new file mode 100644 index 00000000..1d20ddf0 --- /dev/null +++ b/app/views/stories/index.html.erb @@ -0,0 +1,21 @@ +<%= link_to 'Logout', destroy_user_session_path, method: :delete %> + +

Top Stories

+ + +

Interesting Stories

+ + diff --git a/bin/dev b/bin/dev new file mode 100755 index 00000000..ad72c7d5 --- /dev/null +++ b/bin/dev @@ -0,0 +1,16 @@ +#!/usr/bin/env sh + +if ! gem list foreman -i --silent; then + echo "Installing foreman..." + gem install foreman +fi + +# Default to port 3000 if not specified +export PORT="${PORT:-3000}" + +# Let the debug gem allow remote connections, +# but avoid loading until `debugger` is called +export RUBY_DEBUG_OPEN="true" +export RUBY_DEBUG_LAZY="true" + +exec foreman start -f Procfile.dev "$@" diff --git a/config/environments/development.rb b/config/environments/development.rb index 5187e221..8985ca3f 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -51,4 +51,11 @@ # Use an evented file watcher to asynchronously detect changes in source code, # routes, locales, etc. This feature depends on the listen gem. config.file_watcher = ActiveSupport::EventedFileUpdateChecker + # config.after_initialize do + # Bullet.enable = true + # Bullet.alert = true + # Bullet.bullet_logger = true + # Bullet.console = true + # Bullet.rails_logger = true + # end end diff --git a/config/environments/test.rb b/config/environments/test.rb index 8e5cbde5..7b6dc4c0 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -5,7 +5,7 @@ # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped # and recreated between test runs. Don't rely on the data there! - config.cache_classes = true + config.cache_classes = false # Do not eager load code on boot. This avoids loading your whole application # just for the purpose of running a single test. If you are using a tool that diff --git a/config/routes.rb b/config/routes.rb index c12ef082..024dbef0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,8 @@ Rails.application.routes.draw do devise_for :users - root to: 'pages#home' + root to: 'stories#index' + resources :stories, only: [:index] do + post :mark_as_interesting, on: :collection + end end + diff --git a/db/migrate/20240722051921_create_stories.rb b/db/migrate/20240722051921_create_stories.rb new file mode 100644 index 00000000..45324f79 --- /dev/null +++ b/db/migrate/20240722051921_create_stories.rb @@ -0,0 +1,11 @@ +class CreateStories < ActiveRecord::Migration[7.0] + def change + create_table :stories do |t| + t.integer :hn_id + t.string :title + t.string :url + + t.timestamps + end + end +end diff --git a/db/migrate/20240722052034_create_user_stories.rb b/db/migrate/20240722052034_create_user_stories.rb new file mode 100644 index 00000000..6a2e0f2b --- /dev/null +++ b/db/migrate/20240722052034_create_user_stories.rb @@ -0,0 +1,10 @@ +class CreateUserStories < ActiveRecord::Migration[7.0] + def change + create_table :user_stories do |t| + t.references :user, null: false, foreign_key: true + t.references :story, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index acc34f3b..676e4319 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,10 +10,27 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2018_02_28_212101) do +ActiveRecord::Schema[7.0].define(version: 2024_07_22_052034) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + create_table "stories", force: :cascade do |t| + t.integer "hn_id" + t.string "title" + t.string "url" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "user_stories", force: :cascade do |t| + t.bigint "user_id", null: false + t.bigint "story_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["story_id"], name: "index_user_stories_on_story_id" + t.index ["user_id"], name: "index_user_stories_on_user_id" + end + create_table "users", force: :cascade do |t| t.string "first_name" t.string "last_name" @@ -33,4 +50,6 @@ t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true end + add_foreign_key "user_stories", "stories" + add_foreign_key "user_stories", "users" end diff --git a/spec/factories/stories.rb b/spec/factories/stories.rb new file mode 100644 index 00000000..28e7d3ad --- /dev/null +++ b/spec/factories/stories.rb @@ -0,0 +1,8 @@ +FactoryBot.define do + factory :story do + hn_id { Faker::Number.number(digits: 5) } + title { Faker::Lorem.sentence } + url { Faker::Internet.url } + end +end + diff --git a/spec/factories/user_stories.rb b/spec/factories/user_stories.rb new file mode 100644 index 00000000..2d4b788c --- /dev/null +++ b/spec/factories/user_stories.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :user_story do + user { nil } + story { nil } + end +end diff --git a/spec/factories/users.rb b/spec/factories/users.rb new file mode 100644 index 00000000..1de45683 --- /dev/null +++ b/spec/factories/users.rb @@ -0,0 +1,8 @@ +FactoryBot.define do + factory :user do + first_name { Faker::Name.first_name } + last_name { Faker::Name.last_name } + email { Faker::Internet.email } + password { 'password' } + end +end diff --git a/spec/models/story_spec.rb b/spec/models/story_spec.rb new file mode 100644 index 00000000..dd70e5ba --- /dev/null +++ b/spec/models/story_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Story, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/user_story_spec.rb b/spec/models/user_story_spec.rb new file mode 100644 index 00000000..c931293a --- /dev/null +++ b/spec/models/user_story_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe UserStory, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index bbe1ba57..c20ca7e5 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -1,11 +1,12 @@ # This file is copied to spec/ when you run 'rails generate rspec:install' require 'spec_helper' ENV['RAILS_ENV'] ||= 'test' -require File.expand_path('../../config/environment', __FILE__) +require_relative '../config/environment' # Prevent database truncation if the environment is production abort("The Rails environment is running in production mode!") if Rails.env.production? require 'rspec/rails' # Add additional requires below this line. Rails is not loaded until this point! +require 'devise' # Requires supporting ruby files with custom matchers and macros, etc, in # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are @@ -20,21 +21,32 @@ # directory. Alternatively, in the individual `*_spec.rb` files, manually # require only the support files necessary. # -# Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } +# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f } # Checks for pending migrations and applies them before tests are run. -# If you are not using ActiveRecord, you can remove this line. -ActiveRecord::Migration.maintain_test_schema! - +# If you are not using ActiveRecord, you can remove these lines. +begin + ActiveRecord::Migration.maintain_test_schema! +rescue ActiveRecord::PendingMigrationError => e + puts e.to_s.strip + exit 1 +end RSpec.configure do |config| # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures config.fixture_path = "#{::Rails.root}/spec/fixtures" + config.include FactoryBot::Syntax::Methods + config.include Devise::Test::IntegrationHelpers, type: :system + config.include Devise::Test::IntegrationHelpers, type: :view + config.include Devise::Test::IntegrationHelpers, type: :request # If you're not using ActiveRecord, or you'd prefer not to run each of your # examples within a transaction, remove the following line or assign false # instead of true. config.use_transactional_fixtures = true + # You can uncomment this line to turn off ActiveRecord support entirely. + # config.use_active_record = false + # RSpec Rails can automatically mix in different behaviours to your tests # based on their file location, for example enabling you to call `get` and # `post` in specs under `spec/controllers`. @@ -42,7 +54,7 @@ # You can disable this behaviour by removing the line below, and instead # explicitly tag your specs with their type, e.g.: # - # RSpec.describe UsersController, :type => :controller do + # RSpec.describe UsersController, type: :controller do # # ... # end # diff --git a/spec/requests/stories_spec.rb b/spec/requests/stories_spec.rb new file mode 100644 index 00000000..f1ce0581 --- /dev/null +++ b/spec/requests/stories_spec.rb @@ -0,0 +1,22 @@ +require 'rails_helper' + +RSpec.describe "Stories", type: :request do + describe "GET /index" do + context "when signed in" do + it "returns http success" do + user = FactoryBot.create(:user) + sign_in user + get "/" + expect(response).to have_http_status(:success) + end + end + + context "when not signed in" do + it "redirects to sign in page" do + get "/" + expect(response).to have_http_status(302) + expect(response).to redirect_to(new_user_session_path) + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ce33d66d..a0d40805 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -12,7 +12,7 @@ # the additional setup, and require it from the spec files that actually need # it. # -# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration RSpec.configure do |config| # rspec-expectations config goes here. You can use an alternate # assertion/expectation library such as wrong or the stdlib/minitest @@ -61,9 +61,7 @@ # Limits the available syntax to the non-monkey patched syntax that is # recommended. For more details, see: - # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ - # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ - # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode + # https://relishapp.com/rspec/rspec-core/docs/configuration/zero-monkey-patching-mode config.disable_monkey_patching! # Many RSpec users commonly either run the entire suite or an individual diff --git a/spec/views/stories/index.html.erb_spec.rb b/spec/views/stories/index.html.erb_spec.rb new file mode 100644 index 00000000..5877ac15 --- /dev/null +++ b/spec/views/stories/index.html.erb_spec.rb @@ -0,0 +1,19 @@ +require 'rails_helper' + +RSpec.describe "stories/index.html.erb", type: :view do + before do + @user = FactoryBot.create(:user, first_name: 'Sarah', last_name: 'Duve', email: 'sarah@example.com', password: 'password') + @users = FactoryBot.create_list(:user, 3) + @other_user = @users.first() + @story1 = FactoryBot.create(:story, title: "#{@other_user.first_name} thought this was interesting", users: [@other_user]) + assign(:interesting_stories, [@story1]) + assign(:stories, [@story1]) + end + + it "shows interesting stories and who recommended them" do + render template: "stories/index" + puts rendered + expect(rendered).to match(@story1.title) + expect(rendered).to match("marked by #{ @other_user.first_name }") + end +end