From f59633f4721e118a12a6a5b86cd4bff0ab7f51a6 Mon Sep 17 00:00:00 2001 From: Victor Costa Date: Tue, 9 Jun 2015 12:23:15 -0300 Subject: [PATCH] New feature: manage email templates --- Gemfile | 1 + app/controllers/admin/environment_email_templates_controller.rb | 15 +++++++++++++++ app/controllers/my_profile/email_templates_controller.rb | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ app/controllers/my_profile/profile_email_templates_controller.rb | 16 ++++++++++++++++ app/controllers/my_profile/tasks_controller.rb | 3 +++ app/controllers/public/profile_controller.rb | 1 + app/helpers/application_helper.rb | 2 ++ app/helpers/email_template_helper.rb | 12 ++++++++++++ app/helpers/task_helper.rb | 13 +++++++++++++ app/mailers/task_mailer.rb | 8 ++++++-- app/mailers/user_mailer.rb | 6 +++++- app/models/article.rb | 4 ++++ app/models/change_password.rb | 7 +++++++ app/models/email_template.rb | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ app/models/environment.rb | 6 ++++++ app/models/profile.rb | 7 +++++++ app/models/task.rb | 14 ++++++++++++++ app/views/admin_panel/index.html.erb | 1 + app/views/email_templates/_form.html.erb | 31 +++++++++++++++++++++++++++++++ app/views/email_templates/edit.html.erb | 3 +++ app/views/email_templates/index.html.erb | 27 +++++++++++++++++++++++++++ app/views/email_templates/new.html.erb | 3 +++ app/views/email_templates/show.html.erb | 5 +++++ app/views/profile/send_mail.html.erb | 5 +++++ app/views/profile_editor/index.html.erb | 2 ++ app/views/tasks/_approve_article_accept_details.html.erb | 2 ++ app/views/tasks/_task_reject_details.html.erb | 2 ++ db/migrate/20160311184534_create_email_template.rb | 12 ++++++++++++ db/schema.rb | 11 +++++++++++ public/javascripts/application.js | 1 + public/javascripts/email_templates.js | 10 ++++++++++ public/javascripts/tasks.js | 18 ++++++++++++++---- public/proposal-app | 1 + public/stylesheets/email-templates.css | 18 ++++++++++++++++++ test/fixtures/roles.yml | 3 +++ test/functional/environment_email_templates_controller_test.rb | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ test/functional/profile_controller_test.rb | 23 +++++++++++++++++++++++ test/functional/profile_editor_controller_test.rb | 11 +++++++++++ test/functional/profile_email_templates_controller_test.rb | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ test/functional/tasks_controller_test.rb | 26 ++++++++++++++++++++++++++ test/unit/change_password_test.rb | 6 ++++++ test/unit/email_template_helper_test.rb | 20 ++++++++++++++++++++ test/unit/email_template_test.rb | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ test/unit/task_helper_test.rb | 23 +++++++++++++++++++++++ test/unit/task_mailer_test.rb | 47 +++++++++++++++++++++++++++++++++++++++++++++++ test/unit/user_mailer_test.rb | 18 ++++++++++++++++++ 46 files changed, 735 insertions(+), 7 deletions(-) create mode 100644 app/controllers/admin/environment_email_templates_controller.rb create mode 100644 app/controllers/my_profile/email_templates_controller.rb create mode 100644 app/controllers/my_profile/profile_email_templates_controller.rb create mode 100644 app/helpers/email_template_helper.rb create mode 100644 app/helpers/task_helper.rb create mode 100644 app/models/email_template.rb create mode 100644 app/views/email_templates/_form.html.erb create mode 100644 app/views/email_templates/edit.html.erb create mode 100644 app/views/email_templates/index.html.erb create mode 100644 app/views/email_templates/new.html.erb create mode 100644 app/views/email_templates/show.html.erb create mode 100644 db/migrate/20160311184534_create_email_template.rb create mode 100644 public/javascripts/email_templates.js create mode 160000 public/proposal-app create mode 100644 public/stylesheets/email-templates.css create mode 100644 test/functional/environment_email_templates_controller_test.rb create mode 100644 test/functional/profile_email_templates_controller_test.rb create mode 100644 test/unit/email_template_helper_test.rb create mode 100644 test/unit/email_template_test.rb create mode 100644 test/unit/task_helper_test.rb diff --git a/Gemfile b/Gemfile index d16ac58..5bf8e64 100644 --- a/Gemfile +++ b/Gemfile @@ -39,6 +39,7 @@ gem 'grape_logging' gem 'rack-cors' gem 'rack-contrib' gem 'api-pagination', '>= 4.1.1' +gem 'liquid', '~> 3.0.3' # asset pipeline gem 'uglifier', '>= 1.0.3' diff --git a/app/controllers/admin/environment_email_templates_controller.rb b/app/controllers/admin/environment_email_templates_controller.rb new file mode 100644 index 0000000..5326bc0 --- /dev/null +++ b/app/controllers/admin/environment_email_templates_controller.rb @@ -0,0 +1,15 @@ +class EnvironmentEmailTemplatesController < EmailTemplatesController + + protect 'manage_email_templates', :environment + + protected + + def owner + environment + end + + before_filter :only => :index do + @back_to = url_for(:controller => :admin_panel) + end + +end diff --git a/app/controllers/my_profile/email_templates_controller.rb b/app/controllers/my_profile/email_templates_controller.rb new file mode 100644 index 0000000..48eda4e --- /dev/null +++ b/app/controllers/my_profile/email_templates_controller.rb @@ -0,0 +1,62 @@ +class EmailTemplatesController < ApplicationController + + def index + @email_templates = owner.email_templates + end + + def show + @email_template = owner.email_templates.find(params[:id]) + + respond_to do |format| + format.html # show.html.erb + format.json { render json: @email_template } + end + end + + def show_parsed + @email_template = owner.email_templates.find(params[:id]) + template_params = {:profile => owner, :environment => environment} + render json: {:parsed_body => @email_template.parsed_body(template_params), :parsed_subject => @email_template.parsed_subject(template_params)} + end + + def new + @email_template = owner.email_templates.build(:owner => owner) + end + + def edit + @email_template = owner.email_templates.find(params[:id]) + end + + def create + @email_template = owner.email_templates.build(params[:email_template]) + @email_template.owner = owner + + if @email_template.save + session[:notice] = _('Email template was successfully created.') + redirect_to url_for(:action => :index) + else + render action: "new" + end + end + + def update + @email_template = owner.email_templates.find(params[:id]) + + if @email_template.update_attributes(params[:email_template]) + session[:notice] = _('Email template was successfully updated.') + redirect_to url_for(:action => :index) + else + render action: "edit" + end + end + + def destroy + @email_template = owner.email_templates.find(params[:id]) + @email_template.destroy + + respond_to do |format| + format.html { redirect_to url_for(:action => :index)} + format.json { head :no_content } + end + end +end diff --git a/app/controllers/my_profile/profile_email_templates_controller.rb b/app/controllers/my_profile/profile_email_templates_controller.rb new file mode 100644 index 0000000..1aba8a7 --- /dev/null +++ b/app/controllers/my_profile/profile_email_templates_controller.rb @@ -0,0 +1,16 @@ +class ProfileEmailTemplatesController < EmailTemplatesController + + needs_profile + protect 'manage_email_templates', :profile + + protected + + def owner + profile + end + + before_filter :only => :index do + @back_to = url_for(:controller => :profile_editor) + end + +end diff --git a/app/controllers/my_profile/tasks_controller.rb b/app/controllers/my_profile/tasks_controller.rb index f31d673..5dfce4d 100644 --- a/app/controllers/my_profile/tasks_controller.rb +++ b/app/controllers/my_profile/tasks_controller.rb @@ -7,6 +7,9 @@ class TasksController < MyProfileController helper CustomFieldsHelper def index + @rejection_email_templates = profile.email_templates.find_all_by_template_type(:task_rejection) + @acceptance_email_templates = profile.email_templates.find_all_by_template_type(:task_acceptance) + @filter_type = params[:filter_type].presence @filter_text = params[:filter_text].presence @filter_responsible = params[:filter_responsible] diff --git a/app/controllers/public/profile_controller.rb b/app/controllers/public/profile_controller.rb index eec2a08..ee880b1 100644 --- a/app/controllers/public/profile_controller.rb +++ b/app/controllers/public/profile_controller.rb @@ -371,6 +371,7 @@ class ProfileController < PublicController def send_mail @mailing = profile.mailings.build(params[:mailing]) @mailing.data = session[:members_filtered] ? {:members_filtered => session[:members_filtered]} : {} + @email_templates = profile.email_templates.find_all_by_template_type(:organization_members) if request.post? @mailing.locale = locale @mailing.person = user diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 1b5be1b..0519b6c 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -54,6 +54,8 @@ module ApplicationHelper include ThemeLoaderHelper + include TaskHelper + def locale (@page && !@page.language.blank?) ? @page.language : FastGettext.locale end diff --git a/app/helpers/email_template_helper.rb b/app/helpers/email_template_helper.rb new file mode 100644 index 0000000..6f16ee4 --- /dev/null +++ b/app/helpers/email_template_helper.rb @@ -0,0 +1,12 @@ +module EmailTemplateHelper + + def mail_with_template(params={}) + if params[:email_template].present? + params[:body] = params[:email_template].parsed_body(params[:template_params]) + params[:subject] = params[:email_template].parsed_subject(params[:template_params]) + params[:content_type] = "text/html" + end + mail(params.except(:email_template)) + end + +end diff --git a/app/helpers/task_helper.rb b/app/helpers/task_helper.rb new file mode 100644 index 0000000..86242f6 --- /dev/null +++ b/app/helpers/task_helper.rb @@ -0,0 +1,13 @@ +module TaskHelper + + def task_email_template(description, email_templates, task, include_blank=true) + return '' unless email_templates.present? + + content_tag( + :div, + labelled_form_field(description, select_tag("tasks[#{task.id}][task][email_template_id]", options_from_collection_for_select(email_templates, :id, :name), :include_blank => include_blank, 'data-url' => url_for(:controller => 'profile_email_templates', :action => 'show_parsed', :profile => profile.identifier))), + :class => 'template-selection' + ) + end + +end diff --git a/app/mailers/task_mailer.rb b/app/mailers/task_mailer.rb index b2fa21c..ed7801e 100644 --- a/app/mailers/task_mailer.rb +++ b/app/mailers/task_mailer.rb @@ -1,5 +1,7 @@ class TaskMailer < ApplicationMailer + include EmailTemplateHelper + def target_notification(task, message) self.environment = task.environment @@ -38,10 +40,12 @@ class TaskMailer < ApplicationMailer @requestor = task.requestor.name @url = url_for(:host => task.requestor.environment.default_hostname, :controller => 'home') - mail( + mail_with_template( to: task.requestor.notification_emails, from: self.class.generate_from(task), - subject: '[%s] %s' % [task.requestor.environment.name, task.target_notification_description] + subject: '[%s] %s' % [task.requestor.environment.name, task.target_notification_description], + email_template: task.email_template, + template_params: {:environment => task.requestor.environment, :task => task, :message => @message, :url => @url, :requestor => task.requestor} ) end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index 6b9549f..65f8ced 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -1,5 +1,7 @@ class UserMailer < ApplicationMailer + include EmailTemplateHelper + def activation_email_notify(user) self.environment = user.environment @@ -25,10 +27,12 @@ class UserMailer < ApplicationMailer @redirection = (true if user.return_to) @join = (user.community_to_join if user.community_to_join) - mail( + mail_with_template( from: "#{user.environment.name} <#{user.environment.contact_email}>", to: user.email, subject: _("[%s] Activate your account") % [user.environment.name], + template_params: {:environment => user.environment, :activation_code => @activation_code, :redirection => @redirection, :join => @join, :person => user.person, :url => @url}, + email_template: user.environment.email_templates.find_by_template_type(:user_activation), ) end diff --git a/app/models/article.rb b/app/models/article.rb index 0a5b8fc..aff781c 100644 --- a/app/models/article.rb +++ b/app/models/article.rb @@ -844,6 +844,10 @@ class Article < ActiveRecord::Base true end + def to_liquid + HashWithIndifferentAccess.new :name => name, :abstract => abstract, :body => body, :id => id, :parent_id => parent_id, :author => author + end + private def sanitize_tag_list diff --git a/app/models/change_password.rb b/app/models/change_password.rb index 9d1a1dd..cd638ac 100644 --- a/app/models/change_password.rb +++ b/app/models/change_password.rb @@ -28,6 +28,13 @@ class ChangePassword < Task validates_presence_of :password_confirmation, :on => :update, :if => lambda { |change| change.status != Task::Status::CANCELLED } validates_confirmation_of :password, :if => lambda { |change| change.status != Task::Status::CANCELLED } + before_save :set_email_template + + def set_email_template + template = environment.email_templates.find_by_template_type(:user_change_password) + data[:email_template_id] = template.id unless template.nil? + end + def environment requestor.environment unless requestor.nil? end diff --git a/app/models/email_template.rb b/app/models/email_template.rb new file mode 100644 index 0000000..6554bc6 --- /dev/null +++ b/app/models/email_template.rb @@ -0,0 +1,50 @@ +class EmailTemplate < ActiveRecord::Base + + belongs_to :owner, :polymorphic => true + + attr_accessible :template_type, :subject, :body, :owner, :name + + validates_presence_of :name + + validates :name, uniqueness: { scope: [:owner_type, :owner_id] } + + validates :template_type, uniqueness: { scope: [:owner_type, :owner_id] }, if: :unique_by_type? + + def parsed_body(params) + @parsed_body ||= parse(body, params) + end + + def parsed_subject(params) + @parsed_subject ||= parse(subject, params) + end + + def self.available_types + { + :task_rejection => {:description => _('Task Rejection'), :owner_type => Profile}, + :task_acceptance => {:description => _('Task Acceptance'), :owner_type => Profile}, + :organization_members => {:description => _('Organization Members'), :owner_type => Profile}, + :user_activation => {:description => _('User Activation'), :unique => true, :owner_type => Environment}, + :user_change_password => {:description => _('Change User Password'), :unique => true, :owner_type => Environment} + } + end + + def available_types + HashWithIndifferentAccess.new EmailTemplate.available_types.select {|k, v| owner.kind_of?(v[:owner_type])} + end + + def type_description + available_types.fetch(template_type, {})[:description] + end + + def unique_by_type? + available_types.fetch(template_type, {})[:unique] + end + + protected + + def parse(source, params) + template = Liquid::Template.parse(source) + template.render(HashWithIndifferentAccess.new(params)) + end + +end diff --git a/app/models/environment.rb b/app/models/environment.rb index b71e3bb..c3503e7 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -25,6 +25,7 @@ class Environment < ActiveRecord::Base has_many :tasks, :dependent => :destroy, :as => 'target' has_many :search_terms, :as => :context has_many :custom_fields, :dependent => :destroy + has_many :email_templates, :foreign_key => :owner_id IDENTIFY_SCRIPTS = /(php[0-9s]?|[sp]htm[l]?|pl|py|cgi|rb)/ @@ -55,6 +56,7 @@ class Environment < ActiveRecord::Base 'manage_environment_trusted_sites' => N_('Manage environment trusted sites'), 'edit_appearance' => N_('Edit appearance'), 'edit_raw_html_block' => N_('Edit Raw HTML block'), + 'manage_email_templates' => N_('Manage Email Templates'), } module Roles @@ -1010,6 +1012,10 @@ class Environment < ActiveRecord::Base self.licenses.any? end + def to_liquid + HashWithIndifferentAccess.new :name => name + end + private def default_language_available diff --git a/app/models/profile.rb b/app/models/profile.rb index 469ad4c..ebc22ee 100644 --- a/app/models/profile.rb +++ b/app/models/profile.rb @@ -84,6 +84,7 @@ class Profile < ActiveRecord::Base 'invite_members' => N_('Invite members'), 'send_mail_to_members' => N_('Send e-Mail to members'), 'manage_custom_roles' => N_('Manage custom roles'), + 'manage_email_templates' => N_('Manage Email Templates'), } acts_as_accessible @@ -218,6 +219,8 @@ class Profile < ActiveRecord::Base has_many :comments_received, :class_name => 'Comment', :through => :articles, :source => :comments + has_many :email_templates, :foreign_key => :owner_id + # Although this should be a has_one relation, there are no non-silly names for # a foreign key on article to reference the template to which it is # welcome_page... =P @@ -542,6 +545,10 @@ class Profile < ActiveRecord::Base ).order('articles.published_at desc, articles.id desc') end + def to_liquid + HashWithIndifferentAccess.new :name => name, :identifier => identifier + end + class << self # finds a profile by its identifier. This method is a shortcut to diff --git a/app/models/task.rb b/app/models/task.rb index a1c45f5..9c89499 100644 --- a/app/models/task.rb +++ b/app/models/task.rb @@ -41,6 +41,8 @@ class Task < ActiveRecord::Base attr_protected :status + settings_items :email_template_id, :type => :integer + def initialize(*args) super self.status = (args.first ? args.first[:status] : nil) || Task::Status::ACTIVE @@ -273,6 +275,18 @@ class Task < ActiveRecord::Base end end + def email_template + @email_template ||= email_template_id.present? ? EmailTemplate.find_by_id(email_template_id) : nil + end + + def to_liquid + HashWithIndifferentAccess.new({ + :requestor => requestor, + :reject_explanation => reject_explanation, + :code => code + }) + end + scope :pending, -> { where status: Task::Status::ACTIVE } scope :hidden, -> { where status: Task::Status::HIDDEN } scope :finished, -> { where status: Task::Status::FINISHED } diff --git a/app/views/admin_panel/index.html.erb b/app/views/admin_panel/index.html.erb index eab20d5..6dbca60 100644 --- a/app/views/admin_panel/index.html.erb +++ b/app/views/admin_panel/index.html.erb @@ -11,6 +11,7 @@ <%= link_to _('Homepage'), :action => 'set_portal_community' %> <%= link_to _('Licenses'), :controller =>'licenses' %> <%= link_to _('Trusted sites'), :controller =>'trusted_sites' %> + <%= link_to _('Email templates'), :controller =>'environment_email_templates' %>

<%= _('Profiles') %>

diff --git a/app/views/email_templates/_form.html.erb b/app/views/email_templates/_form.html.erb new file mode 100644 index 0000000..6db38a1 --- /dev/null +++ b/app/views/email_templates/_form.html.erb @@ -0,0 +1,31 @@ +<%= form_for(@email_template, :url => {:action => @email_template.persisted? ? :update : :create, :id => @email_template.id}) do |f| %> + + <%= error_messages_for :email_template if @email_template.errors.any? %> + +
+
+ <%= labelled_form_field(_('Template Name:'), f.text_field(:name)) %> + <%= labelled_form_field(_('Template Type:'), f.select(:template_type, @email_template.available_types.map {|k,v| [v[:description], k]}, :include_blank => true)) %> + <%= labelled_form_field(_('Subject:'), f.text_field(:subject)) %> +
+
+ +
+ <%= _('The following parameters may be used in subject and body:') %> +
+
+ {{profile.name}}, {{profile.identifier}}, {{environment.name}} +
+
+ <%= render :file => 'shared/tiny_mce' %> + <%= labelled_form_field(_('Body:'), f.text_area(:body, :class => 'mceEditor')) %> +
+ +
+ <%= submit_button(:save, _('Save')) %> + <%= button(:back, _('Back'), :action => :index) %> +
+ +<% end %> diff --git a/app/views/email_templates/edit.html.erb b/app/views/email_templates/edit.html.erb new file mode 100644 index 0000000..0a9b078 --- /dev/null +++ b/app/views/email_templates/edit.html.erb @@ -0,0 +1,3 @@ +

Editing Email Template

+ +<%= render 'form' %> diff --git a/app/views/email_templates/index.html.erb b/app/views/email_templates/index.html.erb new file mode 100644 index 0000000..45cd2e8 --- /dev/null +++ b/app/views/email_templates/index.html.erb @@ -0,0 +1,27 @@ +
+

<%= _('Email Templates') %>

+ + + + + + + + + <% @email_templates.each do |email_template| %> + + + + + + <% end %> +
<%= _('Name') %><%= _('Type') %><%= _('Actions') %>
<%= email_template.name %><%= email_template.type_description %> + <%= button_without_text(:edit, _('Edit'), {:action => :edit, :id => email_template.id}) %> + <%= button_without_text(:remove, _('Remove'), {:action => :destroy, :id => email_template.id}, method: :delete, data: { confirm: 'Are you sure?' }) %> +
+ +
+ + <%= button(:new, _('New template'), :action => :new) %> + <%= button(:back, _('Back'), @back_to) %> +
diff --git a/app/views/email_templates/new.html.erb b/app/views/email_templates/new.html.erb new file mode 100644 index 0000000..739ff6b --- /dev/null +++ b/app/views/email_templates/new.html.erb @@ -0,0 +1,3 @@ +

New Email Template

+ +<%= render 'form' %> diff --git a/app/views/email_templates/show.html.erb b/app/views/email_templates/show.html.erb new file mode 100644 index 0000000..14493b7 --- /dev/null +++ b/app/views/email_templates/show.html.erb @@ -0,0 +1,5 @@ +

<%= notice %>

+ + +<%= link_to 'Edit', url_for(:action => :edit, :id => @email_template.id) %> | +<%= link_to 'Back', url_for(:action => :index) %> diff --git a/app/views/profile/send_mail.html.erb b/app/views/profile/send_mail.html.erb index b994e88..ce93620 100644 --- a/app/views/profile/send_mail.html.erb +++ b/app/views/profile/send_mail.html.erb @@ -6,6 +6,11 @@ <% to = @mailing.data[:members_filtered].present? ? @mailing.recipients.map{|r| r.name}.join(', ') : _('All members')%> <%= labelled_form_field(_('To:'), text_area(:data, 'members_filtered', :value => to, :rows => 4, :disabled => 'disabled', :class => 'send-mail-recipients')) %> +
+ <% if @email_templates.present? %> + <%= labelled_form_field(_('Select a template:'), select_tag(:template, options_from_collection_for_select(@email_templates, :id, :name), :include_blank => true, 'data-url' => url_for(:controller => 'email_templates', :action => 'show_parsed'))) %> +
+<% end %> <%= form_for :mailing, :url => {:action => 'send_mail'}, :html => {:id => 'mailing-form'} do |f| %> diff --git a/app/views/profile_editor/index.html.erb b/app/views/profile_editor/index.html.erb index 8f51c95..15326f4 100644 --- a/app/views/profile_editor/index.html.erb +++ b/app/views/profile_editor/index.html.erb @@ -72,6 +72,8 @@ <%= control_panel_button(_('Edit welcome page'), 'welcome-page', :action => 'welcome_page') if has_welcome_page %> + <%= control_panel_button(_('Email Templates'), 'email-templates', :controller => :profile_email_templates) if profile.organization? %> + <% @plugins.dispatch(:control_panel_buttons).each do |button| %> <%= control_panel_button(button[:title], button[:icon], button[:url], button[:html_options]) %> <% end %> diff --git a/app/views/tasks/_approve_article_accept_details.html.erb b/app/views/tasks/_approve_article_accept_details.html.erb index d0760c9..389e168 100644 --- a/app/views/tasks/_approve_article_accept_details.html.erb +++ b/app/views/tasks/_approve_article_accept_details.html.erb @@ -1,3 +1,5 @@ +<%= task_email_template(_('Select an acceptance email template:'), @acceptance_email_templates, task) %> + <%= render :file => 'shared/tiny_mce' %> <%= labelled_form_field(_('Create a link'), f.check_box(:create_link)) %> diff --git a/app/views/tasks/_task_reject_details.html.erb b/app/views/tasks/_task_reject_details.html.erb index 13bc80c..bfc8043 100644 --- a/app/views/tasks/_task_reject_details.html.erb +++ b/app/views/tasks/_task_reject_details.html.erb @@ -1 +1,3 @@ +<%= task_email_template(_('Select a rejection email template:'), @rejection_email_templates, task) %> + <%= labelled_form_field(_('Rejection explanation'), f.text_area(:reject_explanation, :rows => 5)) %> diff --git a/db/migrate/20160311184534_create_email_template.rb b/db/migrate/20160311184534_create_email_template.rb new file mode 100644 index 0000000..1da1751 --- /dev/null +++ b/db/migrate/20160311184534_create_email_template.rb @@ -0,0 +1,12 @@ +class CreateEmailTemplate < ActiveRecord::Migration + def change + create_table :email_templates do |t| + t.string :name + t.string :template_type + t.string :subject + t.text :body + t.references :owner, :polymorphic => true + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index fb6a2aa..7de0ff2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -357,6 +357,17 @@ ActiveRecord::Schema.define(version: 20160324132518) do add_index "domains", ["owner_id", "owner_type", "is_default"], name: "index_domains_on_owner_id_and_owner_type_and_is_default", using: :btree add_index "domains", ["owner_id", "owner_type"], name: "index_domains_on_owner_id_and_owner_type", using: :btree + create_table "email_templates", force: :cascade do |t| + t.string "name" + t.string "template_type" + t.string "subject" + t.text "body" + t.integer "owner_id" + t.string "owner_type" + t.datetime "created_at" + t.datetime "updated_at" + end + create_table "environments", force: :cascade do |t| t.string "name" t.string "contact_email" diff --git a/public/javascripts/application.js b/public/javascripts/application.js index d3329ae..a23bd43 100644 --- a/public/javascripts/application.js +++ b/public/javascripts/application.js @@ -33,6 +33,7 @@ *= require require_login.js *= require slick.js *= require block-store.js +*= require email_templates.js */ // lodash configuration diff --git a/public/javascripts/email_templates.js b/public/javascripts/email_templates.js new file mode 100644 index 0000000..87a35d8 --- /dev/null +++ b/public/javascripts/email_templates.js @@ -0,0 +1,10 @@ +jQuery(document).ready(function($) { + $('.template-selection select').change(function() { + if(!$(this).val()) return; + + $.getJSON($(this).data('url'), {id: $(this).val()}, function(data) { + $('#mailing-form #mailing_subject').val(data.parsed_subject); + $('#mailing-form .mceEditor').val(data.parsed_body); + }); + }); +}); diff --git a/public/javascripts/tasks.js b/public/javascripts/tasks.js index 4bc028d..069821a 100644 --- a/public/javascripts/tasks.js +++ b/public/javascripts/tasks.js @@ -2,18 +2,28 @@ $("input.task_accept_radio").click(function(){ task_id = this.getAttribute("task_id"); - $('#on-accept-information-' + task_id).show('fast'); - $('#on-reject-information-' + task_id).hide('fast'); + var accept_container = $('#on-accept-information-' + task_id); + var reject_container = $('#on-reject-information-' + task_id); + + accept_container.show('fast'); + reject_container.hide('fast'); $('#on-skip-information-' + task_id).hide('fast'); $('#custom-field-information-' + task_id).show('fast'); + reject_container.find('input, select').prop('disabled', true); + accept_container.find('input, select').prop('disabled', false); }) $("input.task_reject_radio").click(function(){ task_id = this.getAttribute("task_id"); - $('#on-accept-information-' + task_id).hide('fast'); - $('#on-reject-information-' + task_id).show('fast'); + var accept_container = $('#on-accept-information-' + task_id); + var reject_container = $('#on-reject-information-' + task_id); + + accept_container.hide('fast'); + reject_container.show('fast'); $('#on-skip-information-' + task_id).hide('fast'); $('#custom-field-information-' + task_id).show('fast'); + reject_container.find('input, select').prop('disabled', false); + accept_container.find('input, select').prop('disabled', true); }) $("input.task_skip_radio").click(function(){ diff --git a/public/proposal-app b/public/proposal-app new file mode 160000 index 0000000..e7ad784 --- /dev/null +++ b/public/proposal-app @@ -0,0 +1 @@ +Subproject commit e7ad7849b3ef685639d4c5c0bef7b323255e2afa diff --git a/public/stylesheets/email-templates.css b/public/stylesheets/email-templates.css new file mode 100644 index 0000000..556ecd0 --- /dev/null +++ b/public/stylesheets/email-templates.css @@ -0,0 +1,18 @@ +.template-fields .header-fields, .template-fields .available-params { + width: 48%; + display: inline-block; +} +.template-fields .available-params .values { + color: rgb(111, 111, 111); +} +.template-fields .available-params .reference { + text-align: right; +} +.template-fields .available-params .reference a { + color: rgb(32, 101, 229); + text-decoration: none; + font-size: 10px; +} +.email-templates table td { + text-align: center; +} diff --git a/test/fixtures/roles.yml b/test/fixtures/roles.yml index 11a7b04..ae763d7 100644 --- a/test/fixtures/roles.yml +++ b/test/fixtures/roles.yml @@ -38,6 +38,7 @@ four: - manage_environment_organizations - manage_environment_templates - manage_environment_licenses + - manage_email_templates profile_admin: id: 5 environment_id: 1 @@ -60,6 +61,7 @@ profile_admin: - manage_friends - validate_enterprise - publish_content + - manage_email_templates profile_member: id: 6 environment_id: 1 @@ -101,3 +103,4 @@ environment_administrator: - manage_environment_templates - manage_environment_licenses - edit_raw_html_block + - manage_email_templates diff --git a/test/functional/environment_email_templates_controller_test.rb b/test/functional/environment_email_templates_controller_test.rb new file mode 100644 index 0000000..b7e4a87 --- /dev/null +++ b/test/functional/environment_email_templates_controller_test.rb @@ -0,0 +1,65 @@ +require 'test_helper' + +class EnvironmentEmailTemplatesControllerTest < ActionController::TestCase + + setup do + @email_template = EmailTemplate.create!(:name => 'template', :owner => Environment.default) + person = create_user_with_permission('template_manager', 'manage_email_templates', Environment.default) + login_as(person.user.login) + end + + test "should get index" do + get :index + assert_response :success + assert_not_nil assigns(:email_templates) + end + + test "should get new" do + get :new + assert_response :success + end + + test "should create email_template" do + assert_difference('EmailTemplate.count') do + post :create, email_template: { :name => 'test' } + end + + assert_redirected_to url_for(:action => :index) + end + + test "should show email_template" do + get :show, id: @email_template + assert_response :success + end + + test "should get edit" do + get :edit, id: @email_template + assert_response :success + end + + test "should update email_template" do + put :update, id: @email_template, email_template: { } + assert_redirected_to url_for(:action => :index) + end + + test "should destroy email_template" do + assert_difference('EmailTemplate.count', -1) do + delete :destroy, id: @email_template + end + + assert_redirected_to url_for(:action => :index) + end + + test "should get parsed template" do + environment = Environment.default + @email_template.subject = '{{environment.name}}' + @email_template.body = '{{environment.name}}' + @email_template.save! + get :show_parsed, id: @email_template + assert_response :success + json_response = ActiveSupport::JSON.decode(@response.body) + assert_equal "#{environment.name}", json_response['parsed_subject'] + assert_equal "#{environment.name}", json_response['parsed_body'] + end + +end diff --git a/test/functional/profile_controller_test.rb b/test/functional/profile_controller_test.rb index 834394c..cefa4bd 100644 --- a/test/functional/profile_controller_test.rb +++ b/test/functional/profile_controller_test.rb @@ -1527,6 +1527,29 @@ class ProfileControllerTest < ActionController::TestCase assert_redirected_to :action => 'members' end + should 'display email templates as an option to send mail' do + community = fast_create(Community) + create_user_with_permission('profile_moderator_user', 'send_mail_to_members', community) + login_as('profile_moderator_user') + + template1 = EmailTemplate.create!(:owner => community, :name => "Template 1", :template_type => :organization_members) + template2 = EmailTemplate.create!(:owner => community, :name => "Template 2") + + get :send_mail, :profile => community.identifier, :mailing => {:subject => 'Hello', :body => 'We have some news'} + assert_select '.template-selection' + assert_equal [template1], assigns(:email_templates) + end + + should 'do not display email template selection when there is no template for organization members' do + community = fast_create(Community) + create_user_with_permission('profile_moderator_user', 'send_mail_to_members', community) + login_as('profile_moderator_user') + + get :send_mail, :profile => community.identifier, :mailing => {:subject => 'Hello', :body => 'We have some news'} + assert_select '.template-selection' + assert assigns(:email_templates).empty? + end + should 'show all fields to anonymous user' do viewed = create_user('person_1').person Environment.any_instance.stubs(:active_person_fields).returns(['sex', 'birth_date']) diff --git a/test/functional/profile_editor_controller_test.rb b/test/functional/profile_editor_controller_test.rb index d931ee3..0602082 100644 --- a/test/functional/profile_editor_controller_test.rb +++ b/test/functional/profile_editor_controller_test.rb @@ -623,6 +623,17 @@ class ProfileEditorControllerTest < ActionController::TestCase assert_tag :tag => 'a', :attributes => { :href => '/myprofile/default_user/cms' } end + should 'display email template link for organizations in control panel' do + profile = fast_create(Organization) + get :index, :profile => profile.identifier + assert_tag :tag => 'a', :attributes => { :href => "/myprofile/#{profile.identifier}/profile_email_templates" } + end + + should 'not display email template link in control panel for person' do + get :index, :profile => profile.identifier + assert_no_tag :tag => 'a', :attributes => { :href => "/myprofile/#{profile.identifier}/email_templates" } + end + should 'offer to create blog in control panel' do get :index, :profile => profile.identifier assert_tag :tag => 'a', :attributes => { :href => "/myprofile/default_user/cms/new?type=Blog" } diff --git a/test/functional/profile_email_templates_controller_test.rb b/test/functional/profile_email_templates_controller_test.rb new file mode 100644 index 0000000..612dc15 --- /dev/null +++ b/test/functional/profile_email_templates_controller_test.rb @@ -0,0 +1,68 @@ +require 'test_helper' + +class ProfileEmailTemplatesControllerTest < ActionController::TestCase + + setup do + @profile = fast_create(Community) + @email_template = EmailTemplate.create!(:name => 'template', :owner => @profile) + @person = create_user_with_permission('templatemanager', 'manage_email_templates', @profile) + login_as(@person.user.login) + end + + attr_accessor :profile, :person + + test "should get index" do + get :index, :profile => profile.identifier + assert_response :success + assert_not_nil assigns(:email_templates) + end + + test "should get new" do + get :new, :profile => profile.identifier + assert_response :success + end + + test "should create email_template" do + assert_difference('EmailTemplate.count') do + post :create, email_template: { :name => 'test' }, :profile => profile.identifier + end + + assert_redirected_to url_for(:action => :index) + end + + test "should show email_template" do + get :show, id: @email_template, :profile => profile.identifier + assert_response :success + end + + test "should get edit" do + get :edit, id: @email_template, :profile => profile.identifier + assert_response :success + end + + test "should update email_template" do + put :update, id: @email_template, email_template: { }, :profile => profile.identifier + assert_redirected_to url_for(:action => :index) + end + + test "should destroy email_template" do + assert_difference('EmailTemplate.count', -1) do + delete :destroy, id: @email_template, :profile => profile.identifier + end + + assert_redirected_to url_for(:action => :index) + end + + test "should get parsed template" do + environment = Environment.default + @email_template.subject = '{{profile.name}} - {{profile.identifier}}' + @email_template.body = '{{profile.name}} - {{profile.identifier}} - {{environment.name}}' + @email_template.save! + get :show_parsed, id: @email_template, :profile => profile.identifier + assert_response :success + json_response = ActiveSupport::JSON.decode(@response.body) + assert_equal "#{profile.name} - #{profile.identifier}", json_response['parsed_subject'] + assert_equal "#{profile.name} - #{profile.identifier} - #{environment.name}", json_response['parsed_body'] + end + +end diff --git a/test/functional/tasks_controller_test.rb b/test/functional/tasks_controller_test.rb index cb6d589..a3033ad 100644 --- a/test/functional/tasks_controller_test.rb +++ b/test/functional/tasks_controller_test.rb @@ -704,4 +704,30 @@ class TasksControllerTest < ActionController::TestCase assert_tag :tag=> 'div', :attributes => { :class => 'field-name' }, :content => /great_field: new value for community!/ end + should "display email template selection when accept a task" do + community = fast_create(Community) + @controller.stubs(:profile).returns(community) + person = create_user_with_permission('taskviewer', 'view_tasks', community) + login_as person.user.login + + email_template = EmailTemplate.create!(:name => 'template', :owner => community, :template_type => :task_acceptance) + task = ApproveArticle.create!(:requestor => person, :target => community, :responsible => person) + get :index + assert_select "#on-accept-information-#{task.id} .template-selection" + assert_equal [email_template], assigns(:acceptance_email_templates) + end + + should "display email template selection when reject a task" do + community = fast_create(Community) + @controller.stubs(:profile).returns(community) + person = create_user_with_permission('taskviewer', 'view_tasks', community) + login_as person.user.login + + email_template = EmailTemplate.create!(:name => 'template', :owner => community, :template_type => :task_rejection) + task = ApproveArticle.create!(:requestor => person, :target => community, :responsible => person) + get :index + assert_select "#on-reject-information-#{task.id} .template-selection" + assert_equal [email_template], assigns(:rejection_email_templates) + end + end diff --git a/test/unit/change_password_test.rb b/test/unit/change_password_test.rb index 5157f1f..5a28f66 100644 --- a/test/unit/change_password_test.rb +++ b/test/unit/change_password_test.rb @@ -71,4 +71,10 @@ class ChangePasswordTest < ActiveSupport::TestCase assert_match(/#{task.requestor.name} wants to change its password/, email.subject) end + should 'set email template when it exists' do + template = EmailTemplate.create!(:template_type => :user_change_password, :name => 'template1', :owner => Environment.default) + task = ChangePassword.create!(:requestor => person) + assert_equal template.id, task.email_template_id + end + end diff --git a/test/unit/email_template_helper_test.rb b/test/unit/email_template_helper_test.rb new file mode 100644 index 0000000..1bfb2c8 --- /dev/null +++ b/test/unit/email_template_helper_test.rb @@ -0,0 +1,20 @@ +require_relative "../test_helper" + +class EmailTemplateHelperTest < ActionView::TestCase + + should 'replace body and subject with parsed values from template' do + template = mock + template.expects(:parsed_body).returns('parsed body') + template.expects(:parsed_subject).returns('parsed subject') + params = {:subject => 'subject', :body => 'body', :email_template => template} + expects(:mail).with({:subject => 'parsed subject', :body => 'parsed body', :content_type => 'text/html'}) + mail_with_template(params) + end + + should 'do not change params if there is no email template' do + params = {:subject => 'subject', :body => 'body'} + expects(:mail).with(params) + mail_with_template(params) + end + +end diff --git a/test/unit/email_template_test.rb b/test/unit/email_template_test.rb new file mode 100644 index 0000000..e44d4a6 --- /dev/null +++ b/test/unit/email_template_test.rb @@ -0,0 +1,53 @@ +require_relative "../test_helper" + +class EmailTemplateTest < ActiveSupport::TestCase + + should 'filter templates by type' do + EmailTemplate.create!(:template_type => :type1, :name => 'template1') + EmailTemplate.create!(:template_type => :type2, :name => 'template2') + EmailTemplate.create!(:template_type => :type2, :name => 'template3') + assert_equal ['template2', 'template3'], EmailTemplate.find_all_by_template_type(:type2).map(&:name) + end + + should 'parse body using params' do + template = EmailTemplate.new(:body => 'Hi {{person}}') + assert_equal 'Hi John', template.parsed_body({:person => 'John'}) + end + + should 'parse subject using params' do + template = EmailTemplate.new(:subject => 'Hi {{person}}') + assert_equal 'Hi John', template.parsed_subject({:person => 'John'}) + end + + should 'not create template with the same name of other' do + template1 = EmailTemplate.new(:template_type => :type1, :name => 'template', :owner => Environment.default) + template2 = EmailTemplate.new(:template_type => :type1, :name => 'template', :owner => Environment.default) + assert template1.save + assert !template2.save + end + + should 'not create duplicated template when template type is unique' do + template1 = EmailTemplate.new(:template_type => :user_activation, :name => 'template1', :owner => Environment.default) + template2 = EmailTemplate.new(:template_type => :user_activation, :name => 'template2', :owner => Environment.default) + assert template1.save + assert !template2.save + end + + should 'create duplicated template when template type is not unique' do + template1 = EmailTemplate.new(:template_type => :task_rejection, :name => 'template1', :owner => Environment.default) + template2 = EmailTemplate.new(:template_type => :task_rejection, :name => 'template2', :owner => Environment.default) + assert template1.save + assert template2.save + end + + should 'return available types when the owner is an environment' do + template = EmailTemplate.new(:owner => Environment.default) + assert_equal [:user_activation, :user_change_password], template.available_types.symbolize_keys.keys + end + + should 'return available types when the owner is a profile' do + template = EmailTemplate.new(:owner => Profile.new) + assert_equal [:task_rejection, :task_acceptance, :organization_members], template.available_types.symbolize_keys.keys + end + +end diff --git a/test/unit/task_helper_test.rb b/test/unit/task_helper_test.rb new file mode 100644 index 0000000..289a693 --- /dev/null +++ b/test/unit/task_helper_test.rb @@ -0,0 +1,23 @@ +require_relative "../test_helper" + +class TaskHelperTest < ActionView::TestCase + + include ApplicationHelper + + def setup + @profile = fast_create(Profile) + @task = fast_create(Task, :target_id => @profile.id) + end + + attr_accessor :task, :profile + + should 'return select field for template selection when there is templates to choose' do + email_templates = 3.times.map { EmailTemplate.new } + assert_tag_in_string task_email_template('Description', email_templates, task), :tag => 'div', :attributes => {:class => 'template-selection'} + end + + should 'not return select field for template selection when there is no templates to choose' do + assert task_email_template('Description', [], task).blank? + end + +end diff --git a/test/unit/task_mailer_test.rb b/test/unit/task_mailer_test.rb index 1dbf834..2daa8c3 100644 --- a/test/unit/task_mailer_test.rb +++ b/test/unit/task_mailer_test.rb @@ -123,6 +123,53 @@ class TaskMailerTest < ActiveSupport::TestCase assert_match(/#{url_to_compare}/, mail.body.to_s) end + should 'be able to send rejection notification based on a selected template' do + task = Task.new + task.expects(:task_cancelled_message).returns('the message') + task.reject_explanation = 'explanation' + + profile = fast_create(Community) + email_template = EmailTemplate.create!(:owner => profile, :name => 'Template 1', :subject => 'template subject - {{environment.name}}', :body => 'template body - {{environment.name}} - {{task.requestor.name}} - {{task.reject_explanation}}') + task.email_template_id = email_template.id + + requestor = Profile.new(:name => 'my name') + requestor.expects(:notification_emails).returns(['requestor@example.com']).at_least_once + + task.expects(:requestor).returns(requestor).at_least_once + requestor.expects(:environment).returns(@environment).at_least_once + task.expects(:environment).returns(@environment).at_least_once + + task.send(:send_notification, :cancelled).deliver + assert !ActionMailer::Base.deliveries.empty? + mail = ActionMailer::Base.deliveries.last + assert_match /text\/html/, mail.content_type + assert_equal 'template subject - example', mail.subject.to_s + assert_equal 'template body - example - my name - explanation', mail.body.to_s + end + + should 'be able to send accept notification based on a selected template' do + task = Task.new + task.expects(:task_finished_message).returns('the message') + + profile = fast_create(Community) + email_template = EmailTemplate.create!(:owner => profile, :name => 'Template 1', :subject => 'template subject - {{environment.name}}', :body => 'template body - {{environment.name}} - {{task.requestor.name}}') + task.email_template_id = email_template.id + + requestor = Profile.new(:name => 'my name') + requestor.expects(:notification_emails).returns(['requestor@example.com']).at_least_once + + task.expects(:requestor).returns(requestor).at_least_once + requestor.expects(:environment).returns(@environment).at_least_once + task.expects(:environment).returns(@environment).at_least_once + + task.send(:send_notification, :finished).deliver + assert !ActionMailer::Base.deliveries.empty? + mail = ActionMailer::Base.deliveries.last + assert_match /text\/html/, mail.content_type + assert_equal 'template subject - example', mail.subject.to_s + assert_equal 'template body - example - my name', mail.body.to_s + end + private def read_fixture(action) IO.readlines("#{FIXTURES_PATH}/task_mailer/#{action}") diff --git a/test/unit/user_mailer_test.rb b/test/unit/user_mailer_test.rb index b119336..92770aa 100644 --- a/test/unit/user_mailer_test.rb +++ b/test/unit/user_mailer_test.rb @@ -26,6 +26,24 @@ fast_create(Person)) assert_match /profile\/some-user\/friends\/suggest/, email.body.to_s end + should 'deliver activation code email' do + assert_difference 'ActionMailer::Base.deliveries.size' do + u = create_user('some-user') + UserMailer.activation_code(u).deliver + end + end + + should 'deliver activation code email with template' do + EmailTemplate.create!(:template_type => :user_activation, :name => 'template1', :subject => 'activation template subject', :body => 'activation template body', :owner => Environment.default) + assert_difference 'ActionMailer::Base.deliveries.size' do + u = create_user('some-user') + UserMailer.activation_code(u).deliver + end + mail = ActionMailer::Base.deliveries.last + assert_equal 'activation template subject', mail.subject.to_s + assert_equal 'activation template body', mail.body.to_s + end + private def read_fixture(action) -- libgit2 0.21.2