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)
+ %>
+
+
+ <% resource.errors.full_messages.each do |message| %>
+ - <%= message %>
+ <% end %>
+
+
+<% 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
+
+ <% @stories.each do |story| %>
+ -
+ <%= story['title'] %>
+ <%= button_to 'Mark as Interesting', mark_as_interesting_stories_path(hn_id: story['id'], title: story['title'], url: story['url']), method: :post %>
+
+ <% end %>
+
+
+Interesting Stories
+
+ <% @interesting_stories.each do |story| %>
+ -
+ <%= story.title %> marked by <%= story.users.map(&:first_name).join(', ') %>
+
+ <% end %>
+
+
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