<%= image_tag "logo.svg", width: 48, height: 48, alt: "WNB.rb", class: "mx-auto" %>
@@ -8,12 +8,15 @@
-
+
-
- <%= link_to "Events", admin_events_path, class: "nav-link text-end #{"active text-decoration-underline" if controller_name == "events"}" %>
+ <%= link_to "Events", admin_events_path, class: "nav-link text-end #{"active" if controller_name == "events"}" %>
+
+ -
+ <%= link_to "Speakers", admin_speakers_path, class: "nav-link text-end #{"active" if controller_name == "speakers"}" %>
- <%= button_to "Sign out", destroy_user_session_path, form_class: "text-end",class: "btn btn-sm btn-secondary mb-0", method: :delete %>
+ <%= button_to "Sign out", destroy_user_session_path, form_class: "text-end", class: "btn btn-sm btn-secondary mb-0", method: :delete %>
diff --git a/config/initializers/custom_error.rb b/config/initializers/custom_error.rb
new file mode 100644
index 00000000..41980cee
--- /dev/null
+++ b/config/initializers/custom_error.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+ActionView::Base.field_error_proc = proc do |html_tag, _instance|
+ html_tag.gsub('form-control', 'form-control is-invalid').html_safe
+end
diff --git a/config/routes.rb b/config/routes.rb
index 84fb4ca2..ecc63a36 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -8,8 +8,10 @@
sign_up: 'new',
registration: 'register',
}
+
namespace :admin, constraints: { format: 'html' } do
get 'dashboard', to: 'dashboard#show'
+ resources :speakers, only: %i[index new create edit update]
resources :events, only: %i[index new create edit update destroy]
end
diff --git a/spec/controllers/admin/events_controller_spec.rb b/spec/controllers/admin/events_controller_spec.rb
index 177bbea2..ec847a7b 100644
--- a/spec/controllers/admin/events_controller_spec.rb
+++ b/spec/controllers/admin/events_controller_spec.rb
@@ -3,7 +3,7 @@
require './spec/rails_helper'
RSpec.describe Admin::EventsController, type: :controller do
- describe '#index' do
+ describe 'GET #index' do
context 'when user is not authenticated' do
it 'returns 302' do
get :index
@@ -33,7 +33,7 @@
end
end
- describe '#edit' do
+ describe 'GET #edit' do
context 'when user is not authenticated' do
let(:event) { FactoryBot.create(:event) }
@@ -50,7 +50,7 @@
let(:event) { FactoryBot.create(:event) }
it 'returns 401' do
- get :edit, params: {id: event.id}
+ get :edit, params: { id: event.id }
expect(response).to have_http_status(401)
end
end
@@ -61,18 +61,18 @@
let(:event) { FactoryBot.create(:event) }
it 'returns 200' do
- get :edit, params: {id: event.id}
+ get :edit, params: { id: event.id }
expect(response).to have_http_status(200)
end
end
end
- describe '#update' do
+ describe 'PUT #update' do
context 'when user is not authenticated' do
let(:event) { FactoryBot.create(:event) }
it 'returns 302' do
- post :update, params: { id: event.id }
+ put :update, params: { id: event.id }
expect(response).to have_http_status(302)
expect(response).to redirect_to(new_user_session_path)
end
@@ -109,7 +109,7 @@
end
end
- describe '#create' do
+ describe 'POST #create' do
context 'when user is not authenticated' do
it 'returns 302' do
post :create
@@ -134,7 +134,14 @@
it 'returns 302' do
post :create,
- params: { event: { title: 'My event', description: 'Great event', date: 1.month.from_now, type: 'Meetup' } }
+ params: {
+ event: {
+ title: 'My event',
+ description: 'Great event',
+ date: 1.month.from_now,
+ type: 'Meetup',
+ },
+ }
expect(response).to redirect_to(admin_events_path)
end
@@ -146,18 +153,16 @@
let(:user) { create(:user, :admin) }
let(:event) { create(:event) }
- before(:each) do
- sign_in user
- end
+ before(:each) { sign_in user }
it 'redirects to index page when event exists' do
- delete :destroy, params: {id: event.id}
+ delete :destroy, params: { id: event.id }
expect(response).to redirect_to(admin_events_path)
expect(Event.exists?(event.id)).to be(false)
end
it 'returns 404 when the event does not exist' do
- delete :destroy, params: {id: 'fakefake'}
+ delete :destroy, params: { id: 'fakefake' }
expect(response).to have_http_status(404)
end
end
@@ -166,12 +171,10 @@
let(:user) { create(:user) }
let(:event) { create(:event) }
- before(:each) do
- sign_in user
- end
+ before(:each) { sign_in user }
it 'returns 401' do
- delete :destroy, params: {id: event.id}
+ delete :destroy, params: { id: event.id }
expect(response).to have_http_status(401)
expect(Event.exists?(event.id)).to be(true)
end
@@ -181,7 +184,7 @@
let(:event) { create(:event) }
it 'redirects to login' do
- delete :destroy, params: {id: event.id}
+ delete :destroy, params: { id: event.id }
expect(response).to redirect_to(new_user_session_path)
expect(Event.exists?(event.id)).to be(true)
end
diff --git a/spec/controllers/admin/speakers_controller_spec.rb b/spec/controllers/admin/speakers_controller_spec.rb
new file mode 100644
index 00000000..b376f020
--- /dev/null
+++ b/spec/controllers/admin/speakers_controller_spec.rb
@@ -0,0 +1,160 @@
+# frozen_string_literal: true
+
+require './spec/rails_helper'
+
+RSpec.describe Admin::SpeakersController, type: :controller do
+ describe 'GET #index' do
+ context 'when user is not authenticated' do
+ it 'redirects to login page' do
+ get :index
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ context 'when user is not admin' do
+ it 'returns 401' do
+ sign_in create(:user)
+ get :index
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+
+ context 'when user is admin' do
+ before { sign_in create(:user, :admin) }
+
+ it 'returns a success response' do
+ get :index
+ expect(response).to be_successful
+ end
+ end
+ end
+
+ describe 'GET #new' do
+ context 'when user is admin' do
+ before { sign_in create(:user, :admin) }
+
+ it 'returns a successful response' do
+ get :new
+ expect(response).to be_successful
+ end
+
+ it 'assigns a new speaker to @speaker' do
+ get :new
+ speaker = controller.instance_variable_get(:@speaker)
+ expect(speaker).to be_a(Speaker)
+ expect(speaker).to be_new_record
+ end
+ end
+ end
+
+ describe 'GET #edit' do
+ let(:speaker) { create(:speaker) }
+
+ context 'when user is not authenticated' do
+ it 'redirects to login page' do
+ get :edit, params: { id: 1 }
+ expect(response).to have_http_status(302)
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+
+ context 'when user is not admin' do
+ it 'returns 401' do
+ sign_in create(:user)
+ get :edit, params: { id: 1 }
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+ context 'when user is admin' do
+ before { sign_in create(:user, :admin) }
+ let(:speaker) { FactoryBot.create(:speaker) }
+
+ it 'returns a success response' do
+ get :edit, params: { id: speaker.id }
+ expect(response).to be_successful
+ end
+
+ it 'sets the correct speaker instance variable' do
+ get :edit, params: { id: speaker.id }
+ expect(controller.instance_variable_get(:@speaker)).to eq(speaker)
+ end
+
+ it 'renders not found if speaker is not found' do
+ expect { get :edit, params: { id: 'nonexistent_id' } }.to raise_error(
+ ActiveRecord::RecordNotFound,
+ )
+ end
+ end
+ end
+
+ describe 'POST #create' do
+ let(:valid_params) { attributes_for(:speaker) }
+ let(:invalid_params) { attributes_for(:speaker, name: nil) }
+
+ context 'when user is not authenticated' do
+ it 'returns 302' do
+ post :create
+ expect(response).to have_http_status(302)
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+
+ context 'when user is admin' do
+ before { sign_in create(:user, :admin) }
+ it 'creates a new speaker with valid params' do
+ post :create, params: { speaker: valid_params }
+ expect(Speaker.last.name).to eq(valid_params[:name])
+ expect(response).to have_http_status(:redirect)
+ end
+
+ it 'does not create a new speaker with invalid params' do
+ expect { post :create, params: { speaker: invalid_params } }.not_to change(Speaker, :count)
+ expect(response).to have_http_status(:unprocessable_entity)
+ end
+ end
+ end
+
+ describe 'PATCH #update' do
+ context 'when user is not authenticated' do
+ it 'redirects to login page' do
+ put :update, params: { id: 1 }
+ expect(response).to have_http_status(302)
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+
+ context 'when user is not admin' do
+ before { sign_in create(:user) }
+ it 'returns 401' do
+ put :update, params: { id: 1 }
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+
+ context 'when user is admin' do
+ before { sign_in create(:user, :admin) }
+
+ let(:speaker) { create(:speaker) }
+ let(:valid_params) { { name: 'Updated Name' } }
+ let(:invalid_params) { { name: nil } }
+
+ it 'returns a success response' do
+ put :update, params: { id: speaker.id, speaker: { name: 'new name' } }
+ expect(response).to redirect_to(edit_admin_speaker_path(speaker))
+ expect(speaker.reload.name).to eq('new name')
+ end
+
+ it 'updates the speaker with valid params' do
+ patch :update, params: { id: speaker.id, speaker: valid_params }
+ expect(speaker.reload.name).to eq('Updated Name')
+ expect(response).to have_http_status(:redirect)
+ end
+
+ it 'does not update the speaker with invalid params' do
+ original_name = speaker.name
+ patch :update, params: { id: speaker.id, speaker: invalid_params }
+ expect(speaker.reload.name).to eq(original_name)
+ expect(response).to have_http_status(:unprocessable_entity)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/api/events_controller_spec.rb b/spec/controllers/api/events_controller_spec.rb
index fcf0ad2a..21fe40b9 100644
--- a/spec/controllers/api/events_controller_spec.rb
+++ b/spec/controllers/api/events_controller_spec.rb
@@ -13,30 +13,19 @@
describe 'GET #past' do
before do
- july_meetup =
- Meetup.create(
- title: 'July Meetup',
- location: 'virtual',
- date: DateTime.new(2021, 7, 31, 16).utc,
- )
- Meetup.create(
- title: 'August Meetup',
- location: 'virtual',
- date: DateTime.new(2021, 8, 31, 16).utc,
- )
+ july_meetup = create(:event, title: 'July Meetup', date: DateTime.new(2021, 7, 31, 16).utc)
+ create(:event, title: 'August Meetup', date: DateTime.new(2021, 8, 31, 16).utc)
Panel.create(title: 'Future Panel', location: 'Denver, Colorado', date: DateTime.now + 1.week)
speaker =
- Speaker.create(
+ create(
+ :speaker,
name: 'Speaker Name',
- tagline: 'Software Developer',
- bio: 'Lorem Ipsum',
- image_url: 'https://picsum.photos/200',
links: {
twitter: 'http://example.com/twitter-link',
mastodon: 'http://example.com/mastodon-link',
- personal_website: 'http://example.com/personal-website-link',
- }
+ website: 'http://example.com/personal-website-link',
+ },
)
EventSpeaker.create(
event: july_meetup,
@@ -71,11 +60,13 @@
july_meetup = body['data']['2021']['July'].first
expect(july_meetup['speakers'].first['name']).to eq('Speaker Name')
- expect(july_meetup['speakers'].first['links']).to eq({
- 'twitter' => 'http://example.com/twitter-link',
- 'mastodon' => 'http://example.com/mastodon-link',
- 'personal_website' => 'http://example.com/personal-website-link',
- })
+ expect(july_meetup['speakers'].first['links']).to eq(
+ {
+ 'twitter' => 'http://example.com/twitter-link',
+ 'mastodon' => 'http://example.com/mastodon-link',
+ 'website' => 'http://example.com/personal-website-link',
+ },
+ )
end
it 'includes talk titles' do
@@ -108,25 +99,11 @@
describe 'GET #upcoming' do
before do
august_meetup =
- Meetup.create(
- title: 'August Meetup',
- location: 'virtual',
- date: DateTime.new(2022, 8, 31, 16).utc,
- )
- Meetup.create(
- title: 'September Meetup',
- location: 'virtual',
- date: DateTime.new(2022, 9, 25, 16).utc,
- )
+ create(:event, title: 'August Meetup', date: DateTime.new(2022, 8, 31, 16).utc)
+ create(:event, title: 'September Meetup', date: DateTime.new(2022, 9, 25, 16).utc)
Panel.create(title: 'Future Panel', location: 'Denver, Colorado', date: DateTime.now - 1.week)
- speaker =
- Speaker.create(
- name: 'Speaker Name',
- tagline: 'Software Developer',
- bio: 'Lorem Ipsum',
- image_url: 'https://picsum.photos/200',
- )
+ speaker = create(:speaker, name: 'Speaker Name')
EventSpeaker.create(
event: august_meetup,
speaker: speaker,
diff --git a/spec/factories/speakers.rb b/spec/factories/speakers.rb
new file mode 100644
index 00000000..2ba09a00
--- /dev/null
+++ b/spec/factories/speakers.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+FactoryBot.define do
+ factory :speaker do
+ name { Faker::Name.name }
+ bio { Faker::Lorem.paragraph }
+ tagline { Faker::Job.title }
+ image_url { Faker::Internet.url }
+ end
+
+ after(:build) { |_speaker| allow(HTTParty).to receive(:get).and_return(double(code: 200)) }
+
+ trait :with_invalid_links do
+ after(:build) { |_speaker| allow(HTTParty).to receive(:get).and_return(double(code: 401)) }
+ links { { notwitter: 'not-a-link', twitter: Faker::Internet.url } }
+ end
+
+ trait :with_valid_links do
+ links { { twitter: Faker::Internet.url, other: Faker::Internet.url } }
+ end
+
+ trait :with_empty_links do
+ links { {} }
+ end
+end
diff --git a/spec/models/speaker_spec.rb b/spec/models/speaker_spec.rb
index 1d4412bb..695e2154 100644
--- a/spec/models/speaker_spec.rb
+++ b/spec/models/speaker_spec.rb
@@ -13,4 +13,53 @@
it { is_expected.to validate_presence_of(:bio) }
it { is_expected.to validate_presence_of(:image_url) }
end
-end
\ No newline at end of file
+
+ describe 'scopes' do
+ it 'orders speakers by name' do
+ speaker_b = create(:speaker, name: 'B Speaker')
+ speaker_a = create(:speaker, name: 'A Speaker')
+ expect(Speaker.ordered_by_name).to eq([speaker_a, speaker_b])
+ end
+ end
+ context 'when links are not present' do
+ it 'validates the social media links' do
+ speaker = build(:speaker)
+ speaker1 = build(:speaker, :with_empty_links)
+
+ expect(speaker).to be_valid
+ expect(speaker1).to be_valid
+ end
+ end
+
+ context 'when links are present' do
+ describe 'with valid links' do
+ it 'validates the social media links' do
+ speaker = build(:speaker, :with_valid_links)
+
+ expect(speaker).to be_valid
+ end
+ end
+
+ describe 'with invalid links' do
+ it 'validates the social media links' do
+ speaker = build(:speaker, :with_invalid_links)
+
+ expect(speaker).to be_invalid
+ expect(speaker.errors[:links]).to include('This social media is not allowed')
+ end
+ end
+ end
+
+ describe 'callbacks' do
+ describe '#format_links' do
+ let(:links) { { 'Twitter' => 'link_twitter', 'LinkedIn' => 'link_linkedIn' } }
+
+ it 'downcases the keys of the links' do
+ speaker = build(:speaker, links: links)
+
+ speaker.valid?
+ expect(speaker.links).to eq({ 'twitter' => 'link_twitter', 'linkedin' => 'link_linkedIn' })
+ end
+ end
+ end
+end
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index 3df076cb..9a1aed57 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -8,6 +8,12 @@
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 'factory_bot_rails'
+require 'rspec/mocks'
+
+# FactoryBot.factories.clear
+# FactoryBot.reload
+FactoryBot::SyntaxRunner.class_eval { include RSpec::Mocks::ExampleMethods }
# Requires supporting ruby files with custom matchers and macros, etc, in
# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
diff --git a/spec/system/managing_events_spec.rb b/spec/system/managing_events_spec.rb
index 91cdfecc..841439d1 100644
--- a/spec/system/managing_events_spec.rb
+++ b/spec/system/managing_events_spec.rb
@@ -36,7 +36,7 @@
describe 'with invalid data' do
it 'shows errors' do
click_on 'Save'
- # byebug
+
expect(page).to have_current_path(admin_events_path)
expect(page).to have_text("Title can't be blank")
end
diff --git a/spec/system/managing_speakers_spec.rb b/spec/system/managing_speakers_spec.rb
new file mode 100644
index 00000000..e6cfeb47
--- /dev/null
+++ b/spec/system/managing_speakers_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+require 'rails_helper'
+
+RSpec.describe 'Managing speakers', type: :system do
+ let(:admin) { create(:user, :admin) }
+ let!(:speaker) { create(:speaker) }
+
+ before do
+ driven_by(:rack_test)
+
+ login_as admin
+ visit admin_speakers_path
+ end
+ context 'create a speaker' do
+ before { click_on 'Create new Speaker' }
+
+ it { expect(page).to have_text('New Speaker') }
+
+ describe 'with valid data' do
+ it 'creates an event' do
+ new_speaker = build(:speaker)
+
+ fill_in 'Name', with: new_speaker.name
+ fill_in 'Bio', with: new_speaker.bio
+ fill_in 'Tagline', with: new_speaker.tagline
+ fill_in 'Image link', with: new_speaker.image_url
+
+ click_on 'Save'
+
+ expect(page).to have_text('Speaker was successfully created.')
+ expect(page).to have_current_path(edit_admin_speaker_url(Speaker.last.id))
+ end
+ end
+
+ describe 'with invalid data' do
+ it 'shows errors' do
+ fill_in 'Name', with: ''
+
+ click_on 'Save'
+
+ expect(page).to have_text("Name can't be blank")
+ end
+ end
+ end
+end