diff --git a/.gitignore b/.gitignore index 1c33e1fe..9c78fbf9 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,6 @@ config/database.yml /nbproject/private/ /.DS_Store .vagrant + +# Ignore application configuration +/config/application.yml diff --git a/Gemfile b/Gemfile index e689b563..be7468bd 100644 --- a/Gemfile +++ b/Gemfile @@ -78,6 +78,9 @@ gem 'rubocop', '~> 0.29.1' # coveralls.io gem 'coveralls', require: false +# Simple, Heroku-friendly Rails app configuration using ENV and a single YAML file +gem 'figaro' + group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug' diff --git a/Gemfile.lock b/Gemfile.lock index 823a5e50..0fe7d968 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -101,6 +101,8 @@ GEM factory_girl_rails (4.7.0) factory_girl (~> 4.7.0) railties (>= 3.0.0) + figaro (1.1.1) + thor (~> 0.14) globalid (0.3.7) activesupport (>= 4.1.0) has_scope (0.7.0) @@ -282,6 +284,7 @@ DEPENDENCIES devise-bootstrap-views devise_openid_authenticatable factory_girl_rails + figaro has_scope jquery-rails jquery-turbolinks @@ -303,5 +306,8 @@ DEPENDENCIES twitter-bootstrap-rails web-console (~> 2.0) +RUBY VERSION + ruby 2.2.2p95 + BUNDLED WITH 1.13.6 diff --git a/app/assets/javascripts/events.js b/app/assets/javascripts/events.js index 9ddf9f8f..c4e5a89a 100644 --- a/app/assets/javascripts/events.js +++ b/app/assets/javascripts/events.js @@ -1,6 +1,47 @@ // Place all the behaviors and hooks related to the matching controller here. // All this logic will automatically be available in application.js. +jQuery(function() { + + $('#send-emails-modal').on('show.bs.modal', function (event) { + var button = $(event.relatedTarget); + var header = button.data('title'); + var list = button.data('list'); + var modal = $(this); + modal.find('.modal-title').text(header); + modal.find('#send-emails-mailto').attr('href', 'mailto:' + list); + modal.find('#send-emails-list').val(list); + }); + + $('#send-emails-clipboard').click(function () { + var $temp = $(""); + $('body').append($temp); + $temp.val($('#send-emails-list').val()).select(); + try { + var successful = document.execCommand('copy'); + var msg = successful ? 'successful' : 'unsuccessful'; + console.log('Copying emails to the clipboard was ' + msg); + } + catch (err) { + console.log('Unable to copy emails to the clipboard'); + } + $temp.remove(); + }); +}); + +$(document).on('turbolinks:load', function() { + $('#event-add-date-picker').bind('click', function() { + // insert our template to the ui and add a remove button + $(EVENT_DATE_PICKER_TEMPLATE) + .insertBefore(this) + .append(' ×') + .find('.close') + .click(function() { + $(this).parent('div').remove(); + }); + }); +}); + function addEventDatePicker() { var picker = $('#event-add-date-picker'); diff --git a/app/assets/stylesheets/events.css b/app/assets/stylesheets/events.css index 06b573df..80b9c9dc 100644 --- a/app/assets/stylesheets/events.css +++ b/app/assets/stylesheets/events.css @@ -20,3 +20,10 @@ #applicants_overview { margin-bottom: 20px; } + +.tooltip-wrapper { + display: inline-block; +} +.tooltip .btn[disabled] { + pointer-events: none; +} diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 7bd8a62f..99ab9faf 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -12,6 +12,8 @@ def dropdown_items # everyone gets their profile, if it exists if current_user.profile.present? o << (menu_item t(:profile, scope: 'navbar'), profile_path(current_user.profile)) + else + o << (menu_item t(:create_profile, scope: 'navbar'), new_profile_path) end # pupils get their applications if current_user.role == "pupil" diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb new file mode 100644 index 00000000..3af062c8 --- /dev/null +++ b/app/mailers/application_mailer.rb @@ -0,0 +1,4 @@ +class ApplicationMailer < ActionMailer::Base + default from: "workshop.portal@gmail.com" + layout 'mailer' +end diff --git a/app/mailers/portal_mailer.rb b/app/mailers/portal_mailer.rb new file mode 100644 index 00000000..13e85741 --- /dev/null +++ b/app/mailers/portal_mailer.rb @@ -0,0 +1,17 @@ +class PortalMailer < ApplicationMailer + + # @param hide_recipients [Boolean] - identify whether recipients should be hidden from each other (true) or not (false) + # @param recipients [Array] - email addresses of recipients + # @param reply_to [Array] - email addresses of recipient of the answer + # @param subject [String] - subject of the mail + # @param content [String] - content of the mail + # @return [ActionMailer::MessageDelivery] a mail object with the given parameters. + def generic_email(hide_recipients, recipients, reply_to, subject, content) + @content = content + if (hide_recipients) + mail(bcc: recipients, reply_to: reply_to, subject: subject) + else + mail(to: recipients, reply_to: reply_to, subject: subject) + end + end +end diff --git a/app/models/application_letter.rb b/app/models/application_letter.rb index f4b97eea..7493f822 100644 --- a/app/models/application_letter.rb +++ b/app/models/application_letter.rb @@ -28,6 +28,8 @@ class ApplicationLetter < ActiveRecord::Base enum status: {accepted: 1, rejected: 0, pending: 2} # Checks if the deadline is over + # additionally only return if event and event.application_deadline is present + # TODO: 'event.application_deadline' should never be nil, when #18 is finished. Please remove this in #18. # # @param none # @return [Boolean] true if deadline is over diff --git a/app/models/event.rb b/app/models/event.rb index 558b4b77..28e31a74 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -78,6 +78,32 @@ def agreement_letter_for(user) enum kind: [ :workshop, :camp ] + # Returns whether all application_letters are classified or not + # + # @param none + # @return [Boolean] if status of all application_letters is not pending + def applications_classified? + application_letters.all? { |application_letter| application_letter.status != 'pending' } + end + + # Returns a string of all email addresses of rejected applications + # + # @param none + # @return [String] Concatenation of all email addresses of rejected applications, seperated by ',' + def email_adresses_of_rejected_applicants + rejected_applications = application_letters.where(status: ApplicationLetter.statuses[:rejected]) + rejected_applications.map{ |applications_letter| applications_letter.user.email }.join(',') + end + + # Returns a string of all email addresses of accepted applications + # + # @param none + # @return [String] Concatenation of all email addresses of accepted applications, seperated by ',' + def email_adresses_of_accepted_applicants + accepted_applications = application_letters.where(status: ApplicationLetter.statuses[:accepted]) + accepted_applications.map{ |application_letter| application_letter.user.email }.join(',') + end + # Returns the number of free places of the event, this value may be negative # # @param none diff --git a/app/views/events/_applicants_overview.html.erb b/app/views/events/_applicants_overview.html.erb index 10618040..b73cddff 100644 --- a/app/views/events/_applicants_overview.html.erb +++ b/app/views/events/_applicants_overview.html.erb @@ -1,6 +1,5 @@
- -

<%= t('.title', title: @event.name) %>

+

<%= t '.title', title: @event.name %>

<%= t '.free_places', count: @free_places %>
<%= t '.occupied_places', count: @occupied_places %>
@@ -41,18 +40,54 @@
<%= link_to "Alle annehmen", - events_path, :class => 'btn btn-default'%> + events_path, :class => 'btn btn-default'%> <%= link_to "Alle drucken", - events_path, :class => 'btn btn-default'%> + events_path, :class => 'btn btn-default'%>
- <%= link_to "Zusagen verschicken", - events_path, :class => 'btn btn-default'%> - - <%= link_to "Absagen verschicken", - events_path, :class => 'btn btn-default'%> + <% if (!@event.applications_classified?) %> +
+ <%= button_tag t('.sending_acceptances'), type: 'button', class: 'send-emails-button btn btn-default', disabled: true %> +
+
+ <%= button_tag t('.sending_rejections'), type: 'button', class: 'send-emails-button btn btn-default', disabled: true %> +
+ <% elsif (@event.compute_free_places < 0)%> +
+ <%= button_tag t('.sending_acceptances'), type: 'button', class: 'send-emails-button btn btn-default', disabled: true %> +
+
+ <%= button_tag t('.sending_rejections'), type: 'button', class: 'send-emails-button btn btn-default', disabled: true %> +
+ <% else %> + <%= button_tag t('.sending_acceptances'), type: 'button', class: 'btn btn-default', data: {toggle: 'modal', target: '#send-emails-modal', title: t('.sending_acceptances'), list: @event.email_adresses_of_accepted_applicants} %> + <%= button_tag t('.sending_rejections'), type: 'button', class: 'btn btn-default', data: {toggle: 'modal', target: '#send-emails-modal', title: t('.sending_rejections'), list: @event.email_adresses_of_rejected_applicants} %> + <% end %>
+ + + diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb new file mode 100644 index 00000000..991cf0ff --- /dev/null +++ b/app/views/layouts/mailer.html.erb @@ -0,0 +1,5 @@ + + + <%= yield %> + + diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb new file mode 100644 index 00000000..37f0bddb --- /dev/null +++ b/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/app/views/portal_mailer/generic_email.html.erb b/app/views/portal_mailer/generic_email.html.erb new file mode 100644 index 00000000..228b5714 --- /dev/null +++ b/app/views/portal_mailer/generic_email.html.erb @@ -0,0 +1 @@ +

<%= @content %>

\ No newline at end of file diff --git a/app/views/portal_mailer/generic_email.text.erb b/app/views/portal_mailer/generic_email.text.erb new file mode 100644 index 00000000..c39df4db --- /dev/null +++ b/app/views/portal_mailer/generic_email.text.erb @@ -0,0 +1 @@ +<%= @content %> \ No newline at end of file diff --git a/config/environments/development.rb b/config/environments/development.rb index e9d8fdb8..41c7e633 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -39,4 +39,16 @@ # Raises error for missing translations # config.action_view.raise_on_missing_translations = true + + config.action_mailer.delivery_method = :smtp + # SMTP settings for gmail + config.action_mailer.smtp_settings = { + :address => "smtp.gmail.com", + :port => 587, + :user_name => ENV['gmail_username'], + :password => ENV['gmail_password'], + :authentication => "plain", + :enable_starttls_auto => true + } + end diff --git a/config/environments/production.rb b/config/environments/production.rb index 471ea5ec..c54c3287 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -76,4 +76,16 @@ # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false + + config.action_mailer.delivery_method = :smtp + # SMTP settings for gmail + config.action_mailer.smtp_settings = { + :address => "smtp.gmail.com", + :port => 587, + :user_name => ENV['gmail_username'], + :password => ENV['gmail_password'], + :authentication => "plain", + :enable_starttls_auto => true + } + end diff --git a/config/locales/de.events.yml b/config/locales/de.events.yml index c58af447..b4412f3d 100644 --- a/config/locales/de.events.yml +++ b/config/locales/de.events.yml @@ -14,6 +14,10 @@ de: occupied_places: one: '%{count} Platz belegt' other: '%{count} Plätze belegt' + unclassified_applications_left: 'Bewerbung(en) wurden noch nicht klassifiziert' + maximum_number_of_participants_exeeded: 'Maximale Teilnehmeranzahl wurde überschritten' + sending_acceptances: 'Zusagen verschicken' + sending_rejections: 'Absagen verschicken' participants: participants: "Teilnehmer" show_participants: "Teilnehmerliste" diff --git a/config/locales/de.yml b/config/locales/de.yml index 2840ab6d..be973ec0 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -33,6 +33,7 @@ de: user_management: "Benutzerverwaltung" requests: "Anfragen" profile: "Mein Profil" + create_profile: "Mein Profil anlegen" settings: "Profilinfo" login: "Einloggen" logout: "Ausloggen" diff --git a/db/schema.rb b/db/schema.rb index 1f7bcb8a..f9edbef5 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -14,8 +14,8 @@ ActiveRecord::Schema.define(version: 20161204205355) do create_table "agreement_letters", force: :cascade do |t| - t.integer "user_id" - t.integer "event_id" + t.integer "user_id", null: false + t.integer "event_id", null: false t.string "path", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false diff --git a/db/seeds.rb b/db/seeds.rb index e07b977d..3c66dcde 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -112,6 +112,7 @@ vegan: false, allergic: false, allergies: "", + status: ApplicationLetter.statuses[:pending], user: applicant, event: event ) diff --git a/spec/features/events_spec.rb b/spec/features/events_spec.rb index bb7e22dc..571b7bd6 100644 --- a/spec/features/events_spec.rb +++ b/spec/features/events_spec.rb @@ -22,6 +22,43 @@ expect(page).to have_css("div#occupied_places") end + scenario "logged in as Organizer I want to be unable to send emails if there is any unclassified application left" do + login(:organizer) + @event.update!(max_participants: 1) + @pupil = FactoryGirl.create(:profile) + @pupil.user.role = :pupil + @pending_application = FactoryGirl.create(:application_letter, :event => @event, :user => @pupil.user) + visit event_path(@event) + expect(page).to have_button(I18n.t('events.applicants_overview.sending_acceptances'), disabled: true) + expect(page).to have_button(I18n.t('events.applicants_overview.sending_rejections'), disabled: true) + end + + scenario "logged in as Organizer I want to be unable to send emails if there is a negative number of free places left" do + login(:organizer) + @event.update!(max_participants: 1) + 2.times do |n| + @pupil = FactoryGirl.create(:profile) + @pupil.user.role = :pupil + FactoryGirl.create(:application_letter_accepted, :event => @event, :user => @pupil.user) + end + visit event_path(@event) + expect(page).to have_button(I18n.t('events.applicants_overview.sending_acceptances'), disabled: true) + expect(page).to have_button(I18n.t('events.applicants_overview.sending_rejections'), disabled: true) + end + + scenario "logged in as Organizer I want to open a modal by clicking on sending emails" do + login(:organizer) + @event.update!(max_participants: 2) + 2.times do |n| + @pupil = FactoryGirl.create(:profile) + @pupil.user.role = :pupil + FactoryGirl.create(:application_letter_accepted, :event => @event, :user => @pupil.user) + end + visit event_path(@event) + click_button I18n.t('events.applicants_overview.sending_acceptances') + expect(page).to have_selector('div', :id => 'send-emails-modal') + end + scenario "logged in as Organizer I can see the correct count of free/occupied places" do login(:organizer) @event.update!(max_participants: 1) diff --git a/spec/mailers/portal_mailer_spec.rb b/spec/mailers/portal_mailer_spec.rb new file mode 100644 index 00000000..f877f7b8 --- /dev/null +++ b/spec/mailers/portal_mailer_spec.rb @@ -0,0 +1,48 @@ +require "rails_helper" + +RSpec.describe PortalMailer, type: :mailer do + let(:recipients) { ['test@example.de', 'test2@example.de'] } + let(:reply_to) { ['test3@example.de'] } + let(:subject) {'Subject'} + let(:content) {'Awesome content'} + + describe 'mail sending with direct addressing' do + let(:mail) { described_class.generic_email(false, recipients, reply_to, subject, content).deliver_now } + + it 'sets the subject' do + expect(mail.subject).to eq(subject) + end + + it 'sets the receiver email' do + expect(mail.to).to eq(recipients) + end + + it 'sets the bcc receiver email' do + expect(mail.bcc).to be_nil + end + + it 'sets the reply_to email' do + expect(mail.reply_to).to eq(reply_to) + end + + it 'renders the sender email' do + expect(mail.from).to eq(['workshop.portal@gmail.com']) + end + + it 'sets the content' do + expect(mail.body.encoded).to match(content) + end + end + + describe 'mail sending with bcc addressing' do + let(:mail) { described_class.generic_email(true, recipients, reply_to, subject, content).deliver_now } + + it 'sets the receiver email' do + expect(mail.to).to be_nil + end + + it 'sets the bcc receiver email' do + expect(mail.bcc).to eq(recipients) + end + end +end diff --git a/spec/mailers/previews/portal_mailer_preview.rb b/spec/mailers/previews/portal_mailer_preview.rb new file mode 100644 index 00000000..956f1764 --- /dev/null +++ b/spec/mailers/previews/portal_mailer_preview.rb @@ -0,0 +1,6 @@ +# Preview all emails at http://localhost:3000/rails/mailers/portal_mailer +class PortalMailerPreview < ActionMailer::Preview + def generic_email_preview + PortalMailer.generic_email(true, "receiver@example.com", "reply@to.me", "This is subject", "This is content") + end +end diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index ce4f203f..e25d29d1 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -59,6 +59,28 @@ end + it "checks if there are unclassified applications_letters" do + event = FactoryGirl.create(:event) + accepted_application_letter = FactoryGirl.create(:application_letter_accepted, :event => event, :user => FactoryGirl.create(:user)) + event.application_letters.push(accepted_application_letter) + expect(event.applications_classified?).to eq(true) + + pending_application_letter = FactoryGirl.create(:application_letter, :event => event, :user => FactoryGirl.create(:user)) + event.application_letters.push(pending_application_letter) + expect(event.applications_classified?).to eq(false) + end + + it "computes the email addresses of the accepted and the rejected applications" do + event = FactoryGirl.create(:event) + accepted_application_letter_1 = FactoryGirl.create(:application_letter_accepted, :event => event, :user => FactoryGirl.create(:user)) + accepted_application_letter_2 = FactoryGirl.create(:application_letter_accepted, :event => event, :user => FactoryGirl.create(:user)) + accepted_application_letter_3 = FactoryGirl.create(:application_letter_accepted, :event => event, :user => FactoryGirl.create(:user)) + rejected_application_letter = FactoryGirl.create(:application_letter_rejected, :event => event, :user => FactoryGirl.create(:user)) + [accepted_application_letter_1, accepted_application_letter_2, accepted_application_letter_3, rejected_application_letter].each { |letter| event.application_letters.push(letter) } + expect(event.email_adresses_of_accepted_applicants).to eq([accepted_application_letter_1.user.email, accepted_application_letter_2.user.email, accepted_application_letter_3.user.email].join(',')) + expect(event.email_adresses_of_rejected_applicants).to eq([rejected_application_letter.user.email].join(',')) + end + it "is either a camp or a workshop" do expect { FactoryGirl.build(:event, kind: :smth_invalid) }.to raise_error(ArgumentError) @@ -146,3 +168,4 @@ expect(event.compute_occupied_places).to eq(2) end end + diff --git a/spec/views/navbar_spec.rb b/spec/views/navbar_spec.rb index 25a5d73e..37d7ab50 100644 --- a/spec/views/navbar_spec.rb +++ b/spec/views/navbar_spec.rb @@ -21,11 +21,12 @@ render template: 'application/index', layout: 'layouts/application' end - it "shows Profilinfo, Meine Bewerbungen, Ausloggen in the dropdown" do + it "shows Profilinfo, Mein Profil anlegen, Meine Bewerbungen, Ausloggen in the dropdown" do expect(rendered).to have_css(".nav .dropdown-menu a", text: 'Profilinfo') + expect(rendered).to have_css(".nav .dropdown-menu a", text: 'Mein Profil anlegen') expect(rendered).to have_css(".nav .dropdown-menu a", text: 'Meine Bewerbungen') expect(rendered).to have_css(".nav .dropdown-menu a", text: 'Ausloggen') - expect(rendered).to have_css(".nav .dropdown-menu a", count: 3) + expect(rendered).to have_css(".nav .dropdown-menu a", count: 4) end end