Skip to content
Merged
4 changes: 2 additions & 2 deletions app/assets/javascripts/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@
$(function() {
$("body").removeClass("no-js");

$('#event_local_date, #meeting_local_date, #workshop_local_date, #workshop_rsvp_open_local_date').pickadate({
$('#event_local_date, #meeting_local_date, #workshop_local_date, #workshop_rsvp_open_local_date, #workshop_rsvp_close_local_date').pickadate({
format: 'dd/mm/yyyy'
});
$('#announcement_expires_at, #ban_expires_at').pickadate();
$(
"#meeting_local_time, #meeting_local_end_time, #event_local_time, #event_local_end_time, #workshop_local_time, #workshop_local_end_time, #workshop_rsvp_open_local_time"
"#meeting_local_time, #meeting_local_end_time, #event_local_time, #event_local_end_time, #workshop_local_time, #workshop_local_end_time, #workshop_rsvp_open_local_time, #workshop_rsvp_close_local_time"
).pickatime({
format: "HH:i",
});
Expand Down
1 change: 1 addition & 0 deletions app/controllers/admin/workshops_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ def workshop_params
:local_date, :local_time, :local_end_time, :chapter_id,
:invitable, :seats, :virtual, :slack_channel, :slack_channel_link,
:rsvp_open_local_date, :rsvp_open_local_time, :description,
:rsvp_close_local_date, :rsvp_close_local_time,
:coach_spaces, :student_spaces,
{ sponsor_ids: [] }
])
Expand Down
5 changes: 4 additions & 1 deletion app/controllers/workshop_invitation_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def accept
user = current_user || @invitation.member
workshop = @invitation.workshop
return back_with_message(t('messages.already_rsvped')) if @invitation.attending?
return back_with_message(t('messages.invitations.closed')) unless workshop.rsvp_available?

@invitation.assign_attributes(invitation_params.merge!(attending: true, rsvp_time: Time.zone.now))
return back_with_message(@invitation.errors.full_messages) unless @invitation.valid?
Expand All @@ -51,7 +52,9 @@ def accept
# Inline reject from InvitationControllerConcerns
def reject
@workshop = WorkshopPresenter.decorate(@invitation.workshop)
if @invitation.workshop.date_and_time - 3.5.hours >= Time.zone.now
closes_at = @invitation.workshop.rsvp_closes_at
rsvp_deadline = [@invitation.workshop.date_and_time - 3.5.hours, closes_at].compact.min
if rsvp_deadline >= Time.zone.now
if @invitation.attending.eql? false
redirect_back(fallback_location: invitation_path(@invitation),
notice: t('messages.not_attending_already'))
Expand Down
43 changes: 41 additions & 2 deletions app/models/workshop.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ class Workshop < ApplicationRecord
include Invitable
include Listable

attr_accessor :local_date, :local_time, :local_end_time, :rsvp_open_local_date, :rsvp_open_local_time
attr_accessor :local_date, :local_time, :local_end_time, :rsvp_open_local_date, :rsvp_open_local_time,
:rsvp_close_local_date, :rsvp_close_local_time

resourcify :permissions, role_cname: 'Permission', role_table_name: :permission

Expand Down Expand Up @@ -34,6 +35,9 @@ class Workshop < ApplicationRecord

before_validation :set_date_and_time, :set_end_date_and_time, if: proc { |model| model.chapter_id.present? }
before_validation :set_opens_at
before_validation :set_closes_at
validate :rsvp_date_time_fields_must_be_paired
validate :rsvp_close_before_workshop_start

def host
workshop_host&.sponsor
Expand Down Expand Up @@ -78,7 +82,7 @@ def invitable_yet?
end

def available_for_rsvp?
invitable_yet? && date_and_time.future?
invitable_yet? && rsvp_available?
end

# Is this person attending this event?
Expand Down Expand Up @@ -117,10 +121,45 @@ def rsvp_opens_at
super.in_time_zone(time_zone)
end

def rsvp_closes_at
return nil unless super

super.in_time_zone(time_zone)
end

private

def set_opens_at
new_opens_at = datetime_from_fields(rsvp_open_local_date, rsvp_open_local_time)
self.rsvp_opens_at = new_opens_at if new_opens_at
end

def set_closes_at
new_closes_at = datetime_from_fields(rsvp_close_local_date, rsvp_close_local_time)
self.rsvp_closes_at = new_closes_at if new_closes_at
end

def rsvp_date_time_fields_must_be_paired
%i[rsvp_open rsvp_close].each do |prefix|
date_field = :"#{prefix}_local_date"
time_field = :"#{prefix}_local_time"
date_val = public_send(date_field)
time_val = public_send(time_field)

next if date_val.blank? && time_val.blank?
next if date_val.present? && time_val.present?

if date_val.blank?
errors.add(date_field, "must be provided together with #{prefix.to_s.humanize.downcase} time")
else
errors.add(time_field, "must be provided together with #{prefix.to_s.humanize.downcase} date")
end
end
end

def rsvp_close_before_workshop_start
return unless rsvp_closes_at && date_and_time
return if rsvp_closes_at < date_and_time
errors.add(:rsvp_close_local_date, "must be before the workshop start time")
end
end
15 changes: 12 additions & 3 deletions app/views/admin/workshops/_shared_form.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@
.col-12
.card.bg-light.border-info.mb-3
.card-body
%p= t('admin.workshop.form.rsvp_details')
= f.input :rsvp_open_local_date, as: :string, input_html: { data: { value: @workshop.rsvp_opens_at.try(:strftime, '%d/%m/%Y') } }
= f.input :rsvp_open_local_time, as: :string, input_html: { data: { value: @workshop.rsvp_opens_at.try(:time).try(:strftime, '%H:%M') } }
%h6.card-title RSVP Timing
%p.small.text-muted= t('admin.workshop.form.rsvp_details')
.row
.col-12.col-md-6
= f.input :rsvp_open_local_date, label: 'Open date', as: :string, input_html: { data: { value: @workshop.rsvp_opens_at.try(:strftime, '%d/%m/%Y') } }
.col-12.col-md-6
= f.input :rsvp_open_local_time, label: 'Open time', as: :string, input_html: { data: { value: @workshop.rsvp_opens_at.try(:time).try(:strftime, '%H:%M') } }
.row
.col-12.col-md-6
= f.input :rsvp_close_local_date, label: 'Close date', as: :string, input_html: { data: { value: @workshop.rsvp_closes_at.try(:strftime, '%d/%m/%Y') } }
.col-12.col-md-6
= f.input :rsvp_close_local_time, label: 'Close time', as: :string, input_html: { data: { value: @workshop.rsvp_closes_at.try(:time).try(:strftime, '%H:%M') } }
4 changes: 4 additions & 0 deletions app/views/admin/workshops/show.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@
- else
RSVPs will open at
= @workshop.rsvp_opens_at.strftime('%H:%M on %d/%m/%Y.')
- if @workshop.rsvp_closes_at
%div.mb-2
RSVPs will close at
= @workshop.rsvp_closes_at.strftime('%H:%M on %d/%m/%Y.')
- if @workshop.venue.present? || @workshop.virtual?
%span.badge.bg-info #{@workshop.student_spaces} student spots, #{@workshop.coach_spaces} coach spots
- unless @workshop.spaces?
Expand Down
3 changes: 2 additions & 1 deletion config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ en:
spot_confirmed: "Your spot has been confirmed for %{event}! We look forward to seeing you there."
spot_not_confirmed: "Your spot has not yet been confirmed. We will verify your attendance after you complete the questionnaire."
updated_details: "Invitation details successfully updated."
closed: "RSVPs for this workshop are now closed."
invalid_format: "The requested format is invalid: %{invalid_format}"
notifications:
provider_already_connected: "You are already signed in!"
Expand Down Expand Up @@ -665,7 +666,7 @@ en:
failure: "Workshop cannot be deleted. This is because it has invitees and/or workshop information is on the website for a while long enough that it cannot be deleted."
form:
virtual_info: To create a virtual workshop you must check the box and fill in the below fields. The slack channel will be available on the invitation page for anyone who successfuly RSVPs.
rsvp_details: Fill out these fields if you want RSVPs to open automatically at a future time. Leave blank if you'll handle it manually later.
rsvp_details: Set a date and time (in the chapter's timezone) for RSVPs to open and close automatically. Leave blank to handle manually.
not_invitable: "The workshop is not invitable"
virtual:
details:
Expand Down
8 changes: 8 additions & 0 deletions spec/controllers/admin/workshops_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@
end
end

describe 'POST #create' do
it 'permits rsvp_close_local_date and rsvp_close_local_time' do
expect do
post :create, params: { workshop: { rsvp_close_local_date: '01/12/2020', rsvp_close_local_time: '15:00', host: '' } }
end.not_to raise_error
end
end

describe 'DELETE #destroy' do
context 'workshop invitations have been sent' do
before do
Expand Down
12 changes: 12 additions & 0 deletions spec/features/accepting_invitation_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@

it_behaves_like 'invitation route'

context 'when RSVPs are closed' do
scenario 'cannot accept an invitation after the close time' do
invitation.workshop.update(rsvp_closes_at: 1.hour.ago)

visit invitation_route
click_on 'Attend'

expect(page).to have_content('RSVPs for this workshop are now closed.')
expect(invitation.reload.attending).not_to be(true)
end
end

context 'amend invitation details' do
context 'a student' do
scenario 'cannot accept an invitation without a tutorial' do
Expand Down
109 changes: 109 additions & 0 deletions spec/models/workshop_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,97 @@
it { is_expected.to validate_numericality_of(:student_spaces).is_greater_than(0) }
it { is_expected.to validate_numericality_of(:coach_spaces).is_greater_than(0) }
end

context '#rsvp_date_time_fields_must_be_paired' do
shared_examples_for 'date_time_fields_must_be_paired' do |prefix|
let(:date_field) { :"#{prefix}_local_date" }
let(:time_field) { :"#{prefix}_local_time" }

context 'when both fields are blank' do
before do
workshop.public_send(:"#{prefix}_local_date=", nil)
workshop.public_send(:"#{prefix}_local_time=", nil)
end

it 'is valid' do
workshop.valid?
expect(workshop.errors[date_field]).to be_empty
expect(workshop.errors[time_field]).to be_empty
end
end

context 'when both fields are present' do
before do
workshop.public_send(:"#{prefix}_local_date=", '12/06/2015')
workshop.public_send(:"#{prefix}_local_time=", '18:30')
end

it 'is valid' do
workshop.valid?
expect(workshop.errors[date_field]).to be_empty
expect(workshop.errors[time_field]).to be_empty
end
end

context 'when only date is missing' do
before do
workshop.public_send(:"#{prefix}_local_date=", nil)
workshop.public_send(:"#{prefix}_local_time=", '18:30')
end

it 'is invalid' do
workshop.valid?
expect(workshop.errors[date_field]).to include(
"must be provided together with #{prefix.humanize.downcase} time"
)
end
end

context 'when only time is missing' do
before do
workshop.public_send(:"#{prefix}_local_date=", '12/06/2015')
workshop.public_send(:"#{prefix}_local_time=", nil)
end

it 'is invalid' do
workshop.valid?
expect(workshop.errors[time_field]).to include(
"must be provided together with #{prefix.humanize.downcase} date"
)
end
end
end

context 'with rsvp_open' do
it_should_behave_like 'date_time_fields_must_be_paired', 'rsvp_open'
end

context 'with rsvp_close' do
it_should_behave_like 'date_time_fields_must_be_paired', 'rsvp_close'
end
end

context '#rsvp_closes_at' do
it 'must be before the workshop start time' do
workshop.date_and_time = Time.zone.now + 1.hour
workshop.rsvp_closes_at = Time.zone.now + 2.hours

workshop.valid?
expect(workshop.errors[:rsvp_close_local_date]).to include("must be before the workshop start time")
end

it 'is valid when close time is before workshop start' do
workshop.date_and_time = Time.zone.now + 2.hours
workshop.rsvp_closes_at = Time.zone.now + 1.hour

expect(workshop.valid?).to be(true)
end

it 'is valid when no close time is set' do
workshop.rsvp_closes_at = nil
expect(workshop.valid?).to be(true)
end
end
end

context 'time zone fields' do
Expand Down Expand Up @@ -87,6 +178,24 @@
expect(workshop.rsvp_opens_at.zone).to eq('PDT')
end
end

context 'rsvp_closes_at' do
it 'saves the local time in UTC' do
workshop.update!(
rsvp_close_local_date: '12/06/2015',
rsvp_close_local_time: '18:30'
)

expect(workshop.read_attribute(:rsvp_closes_at)).to eq(utc_time)
end

it 'retrieves the local time from the saved UTC value' do
workshop.update_attribute(:rsvp_closes_at, utc_time)

expect(workshop.rsvp_closes_at).to eq(pacific_time)
expect(workshop.rsvp_closes_at.zone).to eq('PDT')
end
end
end

context '#rsvp_available?' do
Expand Down