diff --git a/app/controllers/concerns/invitation_controller_concerns.rb b/app/controllers/concerns/invitation_controller_concerns.rb new file mode 100644 index 000000000..18766f107 --- /dev/null +++ b/app/controllers/concerns/invitation_controller_concerns.rb @@ -0,0 +1,35 @@ +module InvitationControllerConcerns + extend ActiveSupport::Concern + + included do + before_action :set_invitation + + include InstanceMethods + end + + module InstanceMethods + + def accept + if @invitation.attending.eql? true + redirect_to root_path, notice: t("messages.already_rsvped") + + elsif has_remaining_seats?(@invitation) + @invitation.update_attribute(:attending, true) + + redirect_to root_path, notice: t("messages.accepted_invitation", + name: @invitation.member.name) + else + redirect_to root_path, notice: t("messages.no_available_seats") + end + end + + def reject + if @invitation.attending.eql? false + redirect_to root_path, notice: t("messages.not_attending_already") + else + @invitation.update_attribute(:attending, false) + redirect_to root_path, notice: t("messages.rejected_invitation", name: @invitation.member.name) + end + end + end +end diff --git a/app/controllers/course/invitation_controller.rb b/app/controllers/course/invitation_controller.rb index af424ed20..0234657bc 100644 --- a/app/controllers/course/invitation_controller.rb +++ b/app/controllers/course/invitation_controller.rb @@ -1,22 +1,11 @@ class Course::InvitationController < ApplicationController + include InvitationControllerConcerns - def accept + private + def set_invitation @invitation = CourseInvitation.find_by_token(params[:id]) - - if @invitation.attending.eql? true - redirect_to root_path, notice: t("messages.already_rsvped") - - elsif has_remaining_seats?(@invitation) - @invitation.update_attribute(:attending, true) - - redirect_to root_path, notice: t("messages.accepted_invitation", - name: @invitation.member.name) - else - redirect_to root_path, notice: t("messages.no_available_seats") - end end - private def has_remaining_seats? invitation invitation.course.seats > invitation.course.attending_invitations.length end diff --git a/app/controllers/invitation_controller.rb b/app/controllers/invitation_controller.rb index 54b4de4e7..24e42224f 100644 --- a/app/controllers/invitation_controller.rb +++ b/app/controllers/invitation_controller.rb @@ -1,22 +1,13 @@ class InvitationController < ApplicationController + include InvitationControllerConcerns - def accept - @invitation = SessionInvitation.find_by_token(params[:id]) - - if @invitation.attending.eql? true - redirect_to root_path, notice: t("messages.already_rsvped") - elsif has_remaining_seats?(@invitation) - @invitation.update_attribute(:attending, true) + private - redirect_to root_path, notice: t("messages.accepted_invitation", - name: @invitation.member.name) - else - redirect_to root_path, notice: t("messages.no_available_seats") - end + def set_invitation + @invitation = SessionInvitation.find_by_token(params[:id]) end - private def has_remaining_seats? invitation invitation.sessions.seats > invitation.sessions.attending_invitations.length end diff --git a/app/mailers/session_invitation_mailer.rb b/app/mailers/session_invitation_mailer.rb index 74af0983a..410c3cf7b 100644 --- a/app/mailers/session_invitation_mailer.rb +++ b/app/mailers/session_invitation_mailer.rb @@ -15,6 +15,20 @@ def invite_student sessions, member, invitation end end + def remind_student session, member, invitation + @session = session + @member = member + @invitation = invitation + + load_attachments + + subject = "Reminder for #{@session.title} by Codebar - #{l(@session.date_and_time, format: :email_title)}" + + mail(mail_args(member, subject)) do |format| + format.html + end + end + private def load_attachments diff --git a/app/models/concerns/invitable.rb b/app/models/concerns/invitable.rb new file mode 100644 index 000000000..abf2c547c --- /dev/null +++ b/app/models/concerns/invitable.rb @@ -0,0 +1,15 @@ +module Invitable + extend ActiveSupport::Concern + + included do + include InstanceMethods + end + + module InstanceMethods + + def attending_invitations + invitations.accepted + end + + end +end diff --git a/app/models/course.rb b/app/models/course.rb index 14d16b077..925985f5c 100644 --- a/app/models/course.rb +++ b/app/models/course.rb @@ -1,5 +1,7 @@ class Course < ActiveRecord::Base - has_many :course_invitations + include Invitable + + has_many :invitations, class_name: "CourseInvitation" belongs_to :tutor, class_name: 'Member', foreign_key: 'tutor_id' before_save :set_slug @@ -9,10 +11,6 @@ class Course < ActiveRecord::Base scope :upcoming, -> { where("date_and_time >= ?", DateTime.now).order(:date_and_time) } scope :past, -> { where("date_and_time < ?", DateTime.now).order(:date_and_time) } - def attending_invitations - course_invitations.accepted - end - def to_param slug end diff --git a/app/models/invitation_manager.rb b/app/models/invitation_manager.rb index 94381961b..1a5729a36 100644 --- a/app/models/invitation_manager.rb +++ b/app/models/invitation_manager.rb @@ -12,4 +12,10 @@ def self.send_course_emails course end end + def self.send_session_reminders session + session.attending_invitations.map do |invitation| + invitation.send_reminder + end + end + end diff --git a/app/models/reminders.rb b/app/models/reminders.rb new file mode 100644 index 000000000..bb892e936 --- /dev/null +++ b/app/models/reminders.rb @@ -0,0 +1,8 @@ +class Reminders < ActiveRecord::Base + + scope :session, ->(session) { where(reminder_type: "session", reminder_id: session.id) } + + def self.add_for_session session, count + Reminders.create reminder_type: "session", reminder_id: session.id, count: count + end +end diff --git a/app/models/session_invitation.rb b/app/models/session_invitation.rb index 3c400224b..9a57e272d 100644 --- a/app/models/session_invitation.rb +++ b/app/models/session_invitation.rb @@ -7,6 +7,10 @@ class SessionInvitation < ActiveRecord::Base validates :sessions, :member, presence: true validates :member_id, uniqueness: { scope: [:sessions ] } + def send_reminder + SessionInvitationMailer.remind_student(self.sessions, self.member, self).deliver if attending + end + private def email diff --git a/app/models/sessions.rb b/app/models/sessions.rb index 8f13492f8..2184c9998 100644 --- a/app/models/sessions.rb +++ b/app/models/sessions.rb @@ -1,12 +1,11 @@ class Sessions < ActiveRecord::Base + include Invitable - has_many :session_invitations + has_many :invitations, class_name: "SessionInvitation" has_many :sponsor_sessions has_many :sponsors, through: :sponsor_sessions - scope :upcoming, -> { where("date_and_time > ?", DateTime.now) } + scope :upcoming, -> { where("date_and_time >= ?", DateTime.now).order(:date_and_time) } + scope :next, -> { upcoming.first } - def attending_invitations - session_invitations.accepted - end end diff --git a/app/views/session_invitation_mailer/remind_student.html.erb b/app/views/session_invitation_mailer/remind_student.html.erb new file mode 100644 index 000000000..c6958e066 --- /dev/null +++ b/app/views/session_invitation_mailer/remind_student.html.erb @@ -0,0 +1,33 @@ +

Hi <%= @member.name %>

+ +

<%= @session.title %> by Codebar is almost here!

+

If your are unable to attend, please change your attendance status.

+ +
+
+

<%= @session.title %> by Codebar

+ <%= @session.description %> +

on <%= l(@session.date_and_time, format: :email)%>

+
+

<%= link_to "I can no longer attend", full_url_for(reject_invitation_url(@invitation)), style: "text-decoration:none; color: #663095; font-size: 25px; font-weight: 600; letter-spacing: 0.02em; margin: 10px; text-decoration: none; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7);"%>

+
+
+ + +
+

Location

+

Shutl

+

+ The Courtyard building
+ 11 Curtain Road, 2nd Floor
+ London, EC2A 3LT
+

+ view map +
<%=image_tag(attachments["shutl.png"].url, width: "120px") %>
+
+ +
+ +
+
See you on Wednesday!
+ diff --git a/config/locales/en.yml b/config/locales/en.yml index 560f3a4db..936f8e223 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -13,8 +13,10 @@ en: date: "%d %B %Y" messages: - accepted_invitation: "Thanks for getting back to us %{name}. See you at the meeting!" - already_rsvped: "You have alredy confirmed you attendance for this meeting!" + accepted_invitation: "Thanks for getting back to us %{name}. See you at the session!" + rejected_invitation: "We are so sad you can't make it, but thanks for letting us know %{name}." + already_rsvped: "You have alredy confirmed you attendance!" + not_attending_already: "You have alredy confirmed you can't make it!" no_available_seats: "Unfortunately there are no more spaces left :(. If you would like to join our waiting list send us an email at meetings@codebar.io." member_notifications: "You will be receiving notifications for: %{roles}" no_roles: "You have not signed up for any notifications. If you change your mind, send us an email at meetins@codebar.io so we can change your settings" diff --git a/config/routes.rb b/config/routes.rb index 11789b393..fcdb760fa 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -14,6 +14,7 @@ resources :invitation, only: [] do member do get "accept" + get "reject" end end @@ -21,6 +22,7 @@ resources :invitation, only: [] do member do get "accept" + get "reject" end end end diff --git a/db/migrate/20131104030726_create_reminders.rb b/db/migrate/20131104030726_create_reminders.rb new file mode 100644 index 000000000..d155a8997 --- /dev/null +++ b/db/migrate/20131104030726_create_reminders.rb @@ -0,0 +1,12 @@ +class CreateReminders < ActiveRecord::Migration + def change + create_table :reminders do |t| + t.string :reminder_type + t.string :reminder_id + t.datetime :date_and_time + t.integer :count + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 2434e9270..2bed75bc2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20131103212835) do +ActiveRecord::Schema.define(version: 20131104030726) do create_table "addresses", force: true do |t| t.string "flat" @@ -68,6 +68,15 @@ add_index "members_roles", ["member_id", "role_id"], name: "index_members_roles_on_member_id_and_role_id" add_index "members_roles", ["member_id"], name: "index_members_roles_on_member_id" + create_table "reminders", force: true do |t| + t.string "reminder_type" + t.string "reminder_id" + t.datetime "date_and_time" + t.integer "count" + t.datetime "created_at" + t.datetime "updated_at" + end + create_table "roles", force: true do |t| t.string "name" t.string "description" diff --git a/lib/tasks/reminders.rake b/lib/tasks/reminders.rake new file mode 100644 index 000000000..4ec497c50 --- /dev/null +++ b/lib/tasks/reminders.rake @@ -0,0 +1,13 @@ +namespace :reminders do + desc "Send out reminders" + + task sessions: :environment do + Sessions.upcoming.first.tap do |session| + unless Reminders.session(session).exists? + invitations = InvitationManager.send_session_reminders(session) + + Reminders.add_for_session session, invitations.length + end + end + end +end diff --git a/spec/fabricators/reminder_fabricator.rb b/spec/fabricators/reminder_fabricator.rb new file mode 100644 index 000000000..775ef5a7d --- /dev/null +++ b/spec/fabricators/reminder_fabricator.rb @@ -0,0 +1,7 @@ +Fabricator(:reminders) do + count 10 +end + +Fabricator(:session_reminder, from: :reminders) do + reminder_type "session" +end diff --git a/spec/fabricators/session_invitation_fabricator.rb b/spec/fabricators/session_invitation_fabricator.rb index 8da35aaaa..a205c06e7 100644 --- a/spec/fabricators/session_invitation_fabricator.rb +++ b/spec/fabricators/session_invitation_fabricator.rb @@ -5,3 +5,7 @@ note "I'd love to attend" sessions end + +Fabricator(:attending_session_invitation, from: :session_invitation) do + attending true +end diff --git a/spec/features/accepting_invitation_spec.rb b/spec/features/accepting_invitation_spec.rb index 71ac39beb..d9199611e 100644 --- a/spec/features/accepting_invitation_spec.rb +++ b/spec/features/accepting_invitation_spec.rb @@ -1,10 +1,11 @@ require 'spec_helper' -feature 'a member can accept an invitation' do +feature 'a member can' do - context "session" do + context "#session" do let(:invitation) { Fabricate(:session_invitation) } - let(:invitation_path) { accept_invitation_path(invitation) } + let(:accepting_invitation_path) { accept_invitation_path(invitation) } + let(:rejecting_invitation_path) { reject_invitation_path(invitation) } let(:set_no_available_slots) { invitation.sessions.update_attribute(:seats, 0) } @@ -12,9 +13,10 @@ end - context "course" do + context "#course" do let(:invitation) { Fabricate(:course_invitation) } - let(:invitation_path) { accept_course_invitation_path(invitation) } + let(:accepting_invitation_path) { accept_course_invitation_path(invitation) } + let(:rejecting_invitation_path) { reject_course_invitation_path(invitation) } let(:set_no_available_slots) { invitation.course.update_attribute(:seats, 0) } diff --git a/spec/mailers/session_invitation_mailer_spec.rb b/spec/mailers/session_invitation_mailer_spec.rb index 2a0799b85..bb4b9d81b 100644 --- a/spec/mailers/session_invitation_mailer_spec.rb +++ b/spec/mailers/session_invitation_mailer_spec.rb @@ -3,14 +3,22 @@ describe SessionInvitationMailer do let(:email) { ActionMailer::Base.deliveries.last } + let(:session) { Fabricate(:sessions, title: "HTML & CSS") } + let(:member) { Fabricate(:member) } + let(:invitation) { Fabricate(:session_invitation, sessions: session, member: member) } it "#invite_student" do - member = Fabricate(:member) - sessions = Fabricate(:sessions, title: "HTML & CSS", date_and_time: DateTime.new(2013,10,30,18,30)) invitation_token = "token" - email_subject = "HTML & CSS by Codebar - Wednesday, 30 Oct at 18:30" - SessionInvitationMailer.invite_student(sessions, member, invitation_token).deliver + email_subject = "#{session.title} by Codebar - #{I18n.l(session.date_and_time, format: :email_title)}" + SessionInvitationMailer.invite_student(session, member, invitation).deliver + + expect(email.subject).to eq(email_subject) + end + + it "#remind_student" do + email_subject = "Reminder for #{session.title} by Codebar - #{I18n.l(session.date_and_time, format: :email_title)}" + SessionInvitationMailer.remind_student(session, member, invitation).deliver expect(email.subject).to eq(email_subject) end diff --git a/spec/models/invitation_manager_spec.rb b/spec/models/invitation_manager_spec.rb index b022f196f..fba0bca97 100644 --- a/spec/models/invitation_manager_spec.rb +++ b/spec/models/invitation_manager_spec.rb @@ -3,9 +3,9 @@ describe InvitationManager do let(:students) { 3.times.map { Fabricate(:student) } } + let(:session) { Fabricate(:sessions) } it "#send_session_emails" do - session = Fabricate(:sessions) Member.should_receive(:students).and_return(students) students.each do |student| @@ -25,4 +25,12 @@ InvitationManager.send_course_emails course end + + it "#send_session_reminders" do + Fabricate(:attending_session_invitation, sessions: session) + + SessionInvitation.any_instance.should_receive(:send_reminder) + + InvitationManager.send_session_reminders session + end end diff --git a/spec/models/reminders_spec.rb b/spec/models/reminders_spec.rb new file mode 100644 index 000000000..e660de659 --- /dev/null +++ b/spec/models/reminders_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe Reminders do + let(:session) { Fabricate(:sessions) } + + context "#scopes" do + + it "session" do + Fabricate(:session_reminder, reminder_id: session.id) + + Reminders.session(session).should_not be nil + end + end + + it "adds a reminder for the given session" do + Reminders.add_for_session(session, 2) + + Reminders.session(session).exists?.should eq session.id + end +end diff --git a/spec/models/session_invitation_spec.rb b/spec/models/session_invitation_spec.rb index a05b67311..41e71617d 100644 --- a/spec/models/session_invitation_spec.rb +++ b/spec/models/session_invitation_spec.rb @@ -6,4 +6,13 @@ it_behaves_like InvitationConcerns + it "#send_reminder" do + invitation.update_attribute(:attending, true) + mailer = mock(SessionInvitationMailer, deliver: nil) + + SessionInvitationMailer.should_receive(:remind_student). + with(invitation.sessions, invitation.member, invitation).and_return(mailer) + + invitation.send_reminder + end end diff --git a/spec/models/sessions_spec.rb b/spec/models/sessions_spec.rb index f3ee1ff05..ed3b0e117 100644 --- a/spec/models/sessions_spec.rb +++ b/spec/models/sessions_spec.rb @@ -11,12 +11,20 @@ it { should respond_to(:sponsor_sessions)} context "#scopes" do - it "#upcoming" do + let(:set_upcoming) { 2.times.map { |n| Sessions.create date_and_time: DateTime.now+(n+1).week } } + + before do Sessions.create date_and_time: DateTime.now-1.week - 2.times { |n| Sessions.create date_and_time: DateTime.now+(n+1).week } + set_upcoming + end + it "#upcoming" do Sessions.upcoming.length.should eq 2 end + + it "#next" do + Sessions.next.should eq set_upcoming.first + end end it "#attending" do diff --git a/spec/support/shared_examples/behaves_like_an_invitation_route.rb b/spec/support/shared_examples/behaves_like_an_invitation_route.rb index d43d65e42..b605d0e63 100644 --- a/spec/support/shared_examples/behaves_like_an_invitation_route.rb +++ b/spec/support/shared_examples/behaves_like_an_invitation_route.rb @@ -1,24 +1,45 @@ shared_examples "invitation route" do - scenario 'when there are available spots' do - visit invitation_path + context "accept an invitation" do + scenario 'when there are available spots' do + visit accepting_invitation_path - current_url.should eq root_url - expect(page).to have_content("Thanks for getting back to us #{invitation.member.name}.") - end + current_url.should eq root_url + expect(page).to have_content("Thanks for getting back to us #{invitation.member.name}.") + end + + scenario 'when they have already confirmed their attendance' do + invitation.update_attribute(:attending, true) + visit accepting_invitation_path + + current_url.should eq root_url + expect(page).to have_content("You have alredy confirmed you attendance!") + end - scenario 'when they have already confirmed their attendance' do - invitation.update_attribute(:attending, true) - visit invitation_path + scenario 'when there are available spots' do + set_no_available_slots + visit accepting_invitation_path - current_url.should eq root_url - expect(page).to have_content("You have alredy confirmed you attendance for this meeting!") + current_url.should eq root_url + expect(page).to have_content("Unfortunately there are no more spaces left :(.") + end end - scenario 'when there are available spots' do - set_no_available_slots - visit invitation_path + context "reject an invitation" do + scenario 'when they are succesful' do + invitation.update_attribute(:attending, true) + visit rejecting_invitation_path + + current_url.should eq root_url + expect(page).to have_content(I18n.t("messages.rejected_invitation", name: invitation.member.name)) + end + + scenario 'when already confirmed' do + invitation.update_attribute(:attending, false) + visit rejecting_invitation_path + + current_url.should eq root_url + expect(page).to have_content(I18n.t("messages.not_attending_already")) + end - current_url.should eq root_url - expect(page).to have_content("Unfortunately there are no more spaces left :(.") end end