Commit 8796893a3a68e7ad99691a21d1c2d6a1a5015be9

Authored by Rodrigo Souto
2 parents b113016a b62506a9

Merge branch 'email_template' into 'master'

New feature: manage email templates

This MR depends on liquid ~> 3.0.3 and this version is not available on Debian Jessie, just in testing and unstable. So, I will upload ruby-liquid (3.0.4) to Noosfero Debian Jessie archive to satisfy it.

See merge request !814
Showing 48 changed files with 753 additions and 7 deletions   Show diff stats
Gemfile
... ... @@ -39,6 +39,7 @@ gem 'grape_logging'
39 39 gem 'rack-cors'
40 40 gem 'rack-contrib'
41 41 gem 'api-pagination', '>= 4.1.1'
  42 +gem 'liquid', '~> 3.0.3'
42 43  
43 44 # asset pipeline
44 45 gem 'uglifier', '>= 1.0.3'
... ...
app/controllers/admin/environment_email_templates_controller.rb 0 → 100644
... ... @@ -0,0 +1,15 @@
  1 +class EnvironmentEmailTemplatesController < EmailTemplatesController
  2 +
  3 + protect 'manage_email_templates', :environment
  4 +
  5 + protected
  6 +
  7 + def owner
  8 + environment
  9 + end
  10 +
  11 + before_filter :only => :index do
  12 + @back_to = url_for(:controller => :admin_panel)
  13 + end
  14 +
  15 +end
... ...
app/controllers/my_profile/email_templates_controller.rb 0 → 100644
... ... @@ -0,0 +1,76 @@
  1 +class EmailTemplatesController < ApplicationController
  2 +
  3 + def index
  4 + @email_templates = owner.email_templates
  5 + end
  6 +
  7 + def show
  8 + @email_template = owner.email_templates.find(params[:id])
  9 +
  10 + respond_to do |format|
  11 + format.html # show.html.erb
  12 + format.json { render json: @email_template }
  13 + end
  14 + end
  15 +
  16 + def show_parsed
  17 + @email_template = owner.email_templates.find(params[:id])
  18 + render json: {:parsed_body => @email_template.parsed_body(template_params), :parsed_subject => @email_template.parsed_subject(template_params)}
  19 + end
  20 +
  21 + def new
  22 + @email_template = owner.email_templates.build(:owner => owner)
  23 + @template_params_allowed = template_params_allowed template_params.keys
  24 + end
  25 +
  26 + def edit
  27 + @email_template = owner.email_templates.find(params[:id])
  28 + @template_params_allowed = template_params_allowed template_params.keys
  29 + end
  30 +
  31 + def create
  32 + @email_template = owner.email_templates.build(params[:email_template])
  33 + @email_template.owner = owner
  34 +
  35 + if @email_template.save
  36 + session[:notice] = _('Email template was successfully created.')
  37 + redirect_to url_for(:action => :index)
  38 + else
  39 + render action: "new"
  40 + end
  41 + end
  42 +
  43 + def update
  44 + @email_template = owner.email_templates.find(params[:id])
  45 +
  46 + if @email_template.update_attributes(params[:email_template])
  47 + session[:notice] = _('Email template was successfully updated.')
  48 + redirect_to url_for(:action => :index)
  49 + else
  50 + render action: "edit"
  51 + end
  52 + end
  53 +
  54 + def destroy
  55 + @email_template = owner.email_templates.find(params[:id])
  56 + @email_template.destroy
  57 +
  58 + respond_to do |format|
  59 + format.html { redirect_to url_for(:action => :index)}
  60 + format.json { head :no_content }
  61 + end
  62 + end
  63 +
  64 + private
  65 +
  66 + def template_params
  67 + {:profile_name => current_user.name, :environment_name => environment.name }
  68 + end
  69 +
  70 + def template_params_allowed params
  71 + result = ""
  72 + params.each{ |param| result << "{{ #{param} }} " } if params
  73 + result
  74 + end
  75 +
  76 +end
... ...
app/controllers/my_profile/profile_email_templates_controller.rb 0 → 100644
... ... @@ -0,0 +1,16 @@
  1 +class ProfileEmailTemplatesController < EmailTemplatesController
  2 +
  3 + needs_profile
  4 + protect 'manage_email_templates', :profile
  5 +
  6 + protected
  7 +
  8 + def owner
  9 + profile
  10 + end
  11 +
  12 + before_filter :only => :index do
  13 + @back_to = url_for(:controller => :profile_editor)
  14 + end
  15 +
  16 +end
... ...
app/controllers/my_profile/tasks_controller.rb
... ... @@ -7,6 +7,9 @@ class TasksController &lt; MyProfileController
7 7 helper CustomFieldsHelper
8 8  
9 9 def index
  10 + @rejection_email_templates = profile.email_templates.find_all_by_template_type(:task_rejection)
  11 + @acceptance_email_templates = profile.email_templates.find_all_by_template_type(:task_acceptance)
  12 +
10 13 @filter_type = params[:filter_type].presence
11 14 @filter_text = params[:filter_text].presence
12 15 @filter_responsible = params[:filter_responsible]
... ...
app/controllers/public/profile_controller.rb
... ... @@ -371,6 +371,7 @@ class ProfileController &lt; PublicController
371 371 def send_mail
372 372 @mailing = profile.mailings.build(params[:mailing])
373 373 @mailing.data = session[:members_filtered] ? {:members_filtered => session[:members_filtered]} : {}
  374 + @email_templates = profile.email_templates.find_all_by_template_type(:organization_members)
374 375 if request.post?
375 376 @mailing.locale = locale
376 377 @mailing.person = user
... ...
app/helpers/application_helper.rb
... ... @@ -54,6 +54,8 @@ module ApplicationHelper
54 54  
55 55 include ThemeLoaderHelper
56 56  
  57 + include TaskHelper
  58 +
57 59 def locale
58 60 (@page && !@page.language.blank?) ? @page.language : FastGettext.locale
59 61 end
... ...
app/helpers/email_template_helper.rb 0 → 100644
... ... @@ -0,0 +1,12 @@
  1 +module EmailTemplateHelper
  2 +
  3 + def mail_with_template(params={})
  4 + if params[:email_template].present?
  5 + params[:body] = params[:email_template].parsed_body(params[:template_params])
  6 + params[:subject] = params[:email_template].parsed_subject(params[:template_params])
  7 + params[:content_type] = "text/html"
  8 + end
  9 + mail(params.except(:email_template))
  10 + end
  11 +
  12 +end
... ...
app/helpers/task_helper.rb 0 → 100644
... ... @@ -0,0 +1,13 @@
  1 +module TaskHelper
  2 +
  3 + def task_email_template(description, email_templates, task, include_blank=true)
  4 + return '' unless email_templates.present?
  5 +
  6 + content_tag(
  7 + :div,
  8 + 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))),
  9 + :class => 'template-selection'
  10 + )
  11 + end
  12 +
  13 +end
... ...
app/mailers/task_mailer.rb
1 1 class TaskMailer < ApplicationMailer
2 2  
  3 + include EmailTemplateHelper
  4 +
3 5 def target_notification(task, message)
4 6 self.environment = task.environment
5 7  
... ... @@ -38,10 +40,12 @@ class TaskMailer &lt; ApplicationMailer
38 40 @requestor = task.requestor.name
39 41 @url = url_for(:host => task.requestor.environment.default_hostname, :controller => 'home')
40 42  
41   - mail(
  43 + mail_with_template(
42 44 to: task.requestor.notification_emails,
43 45 from: self.class.generate_from(task),
44   - subject: '[%s] %s' % [task.requestor.environment.name, task.target_notification_description]
  46 + subject: '[%s] %s' % [task.requestor.environment.name, task.target_notification_description],
  47 + email_template: task.email_template,
  48 + template_params: {:environment => task.requestor.environment, :task => task, :message => @message, :url => @url, :requestor => task.requestor}
45 49 )
46 50 end
47 51  
... ...
app/mailers/user_mailer.rb
1 1 class UserMailer < ApplicationMailer
2 2  
  3 + include EmailTemplateHelper
  4 +
3 5 def activation_email_notify(user)
4 6 self.environment = user.environment
5 7  
... ... @@ -25,10 +27,12 @@ class UserMailer &lt; ApplicationMailer
25 27 @redirection = (true if user.return_to)
26 28 @join = (user.community_to_join if user.community_to_join)
27 29  
28   - mail(
  30 + mail_with_template(
29 31 from: "#{user.environment.name} <#{user.environment.contact_email}>",
30 32 to: user.email,
31 33 subject: _("[%s] Activate your account") % [user.environment.name],
  34 + template_params: {:environment => user.environment, :activation_code => @activation_code, :redirection => @redirection, :join => @join, :person => user.person, :url => @url},
  35 + email_template: user.environment.email_templates.find_by_template_type(:user_activation),
32 36 )
33 37 end
34 38  
... ...
app/models/article.rb
... ... @@ -840,6 +840,10 @@ class Article &lt; ActiveRecord::Base
840 840 true
841 841 end
842 842  
  843 + def to_liquid
  844 + HashWithIndifferentAccess.new :name => name, :abstract => abstract, :body => body, :id => id, :parent_id => parent_id, :author => author
  845 + end
  846 +
843 847 private
844 848  
845 849 def sanitize_tag_list
... ...
app/models/change_password.rb
... ... @@ -28,6 +28,13 @@ class ChangePassword &lt; Task
28 28 validates_presence_of :password_confirmation, :on => :update, :if => lambda { |change| change.status != Task::Status::CANCELLED }
29 29 validates_confirmation_of :password, :if => lambda { |change| change.status != Task::Status::CANCELLED }
30 30  
  31 + before_save :set_email_template
  32 +
  33 + def set_email_template
  34 + template = environment.email_templates.find_by_template_type(:user_change_password)
  35 + data[:email_template_id] = template.id unless template.nil?
  36 + end
  37 +
31 38 def environment
32 39 requestor.environment unless requestor.nil?
33 40 end
... ...
app/models/email_template.rb 0 → 100644
... ... @@ -0,0 +1,50 @@
  1 +class EmailTemplate < ActiveRecord::Base
  2 +
  3 + belongs_to :owner, :polymorphic => true
  4 +
  5 + attr_accessible :template_type, :subject, :body, :owner, :name
  6 +
  7 + validates_presence_of :name
  8 +
  9 + validates :name, uniqueness: { scope: [:owner_type, :owner_id] }
  10 +
  11 + validates :template_type, uniqueness: { scope: [:owner_type, :owner_id] }, if: :unique_by_type?
  12 +
  13 + def parsed_body(params)
  14 + @parsed_body ||= parse(body, params)
  15 + end
  16 +
  17 + def parsed_subject(params)
  18 + @parsed_subject ||= parse(subject, params)
  19 + end
  20 +
  21 + def self.available_types
  22 + {
  23 + :task_rejection => {:description => _('Task Rejection'), :owner_type => Profile},
  24 + :task_acceptance => {:description => _('Task Acceptance'), :owner_type => Profile},
  25 + :organization_members => {:description => _('Organization Members'), :owner_type => Profile},
  26 + :user_activation => {:description => _('User Activation'), :unique => true, :owner_type => Environment},
  27 + :user_change_password => {:description => _('Change User Password'), :unique => true, :owner_type => Environment}
  28 + }
  29 + end
  30 +
  31 + def available_types
  32 + HashWithIndifferentAccess.new EmailTemplate.available_types.select {|k, v| owner.kind_of?(v[:owner_type])}
  33 + end
  34 +
  35 + def type_description
  36 + available_types.fetch(template_type, {})[:description]
  37 + end
  38 +
  39 + def unique_by_type?
  40 + available_types.fetch(template_type, {})[:unique]
  41 + end
  42 +
  43 + protected
  44 +
  45 + def parse(source, params)
  46 + template = Liquid::Template.parse(source)
  47 + template.render(HashWithIndifferentAccess.new(params))
  48 + end
  49 +
  50 +end
... ...
app/models/environment.rb
... ... @@ -25,6 +25,7 @@ class Environment &lt; ActiveRecord::Base
25 25 has_many :tasks, :dependent => :destroy, :as => 'target'
26 26 has_many :search_terms, :as => :context
27 27 has_many :custom_fields, :dependent => :destroy
  28 + has_many :email_templates, :foreign_key => :owner_id
28 29  
29 30 IDENTIFY_SCRIPTS = /(php[0-9s]?|[sp]htm[l]?|pl|py|cgi|rb)/
30 31  
... ... @@ -55,6 +56,7 @@ class Environment &lt; ActiveRecord::Base
55 56 'manage_environment_trusted_sites' => N_('Manage environment trusted sites'),
56 57 'edit_appearance' => N_('Edit appearance'),
57 58 'edit_raw_html_block' => N_('Edit Raw HTML block'),
  59 + 'manage_email_templates' => N_('Manage Email Templates'),
58 60 }
59 61  
60 62 module Roles
... ... @@ -1010,6 +1012,10 @@ class Environment &lt; ActiveRecord::Base
1010 1012 self.licenses.any?
1011 1013 end
1012 1014  
  1015 + def to_liquid
  1016 + HashWithIndifferentAccess.new :name => name
  1017 + end
  1018 +
1013 1019 private
1014 1020  
1015 1021 def default_language_available
... ...
app/models/profile.rb
... ... @@ -84,6 +84,7 @@ class Profile &lt; ActiveRecord::Base
84 84 'invite_members' => N_('Invite members'),
85 85 'send_mail_to_members' => N_('Send e-Mail to members'),
86 86 'manage_custom_roles' => N_('Manage custom roles'),
  87 + 'manage_email_templates' => N_('Manage Email Templates'),
87 88 }
88 89  
89 90 acts_as_accessible
... ... @@ -218,6 +219,8 @@ class Profile &lt; ActiveRecord::Base
218 219  
219 220 has_many :comments_received, :class_name => 'Comment', :through => :articles, :source => :comments
220 221  
  222 + has_many :email_templates, :foreign_key => :owner_id
  223 +
221 224 # Although this should be a has_one relation, there are no non-silly names for
222 225 # a foreign key on article to reference the template to which it is
223 226 # welcome_page... =P
... ... @@ -542,6 +545,10 @@ class Profile &lt; ActiveRecord::Base
542 545 ).order('articles.published_at desc, articles.id desc')
543 546 end
544 547  
  548 + def to_liquid
  549 + HashWithIndifferentAccess.new :name => name, :identifier => identifier
  550 + end
  551 +
545 552 class << self
546 553  
547 554 # finds a profile by its identifier. This method is a shortcut to
... ...
app/models/task.rb
... ... @@ -41,6 +41,8 @@ class Task &lt; ActiveRecord::Base
41 41  
42 42 attr_protected :status
43 43  
  44 + settings_items :email_template_id, :type => :integer
  45 +
44 46 def initialize(*args)
45 47 super
46 48 self.status = (args.first ? args.first[:status] : nil) || Task::Status::ACTIVE
... ... @@ -273,6 +275,18 @@ class Task &lt; ActiveRecord::Base
273 275 end
274 276 end
275 277  
  278 + def email_template
  279 + @email_template ||= email_template_id.present? ? EmailTemplate.find_by_id(email_template_id) : nil
  280 + end
  281 +
  282 + def to_liquid
  283 + HashWithIndifferentAccess.new({
  284 + :requestor => requestor,
  285 + :reject_explanation => reject_explanation,
  286 + :code => code
  287 + })
  288 + end
  289 +
276 290 scope :pending, -> { where status: Task::Status::ACTIVE }
277 291 scope :hidden, -> { where status: Task::Status::HIDDEN }
278 292 scope :finished, -> { where status: Task::Status::FINISHED }
... ...
app/views/admin_panel/index.html.erb
... ... @@ -11,6 +11,7 @@
11 11 <tr><td><%= link_to _('Homepage'), :action => 'set_portal_community' %></td></tr>
12 12 <tr><td><%= link_to _('Licenses'), :controller =>'licenses' %></td></tr>
13 13 <tr><td><%= link_to _('Trusted sites'), :controller =>'trusted_sites' %></td></tr>
  14 + <tr><td><%= link_to _('Email templates'), :controller =>'environment_email_templates' %></td></tr>
14 15 </table>
15 16  
16 17 <h2><%= _('Profiles') %></h2>
... ...
app/views/email_templates/_form.html.erb 0 → 100644
... ... @@ -0,0 +1,31 @@
  1 +<%= form_for(@email_template, :url => {:action => @email_template.persisted? ? :update : :create, :id => @email_template.id}) do |f| %>
  2 +
  3 + <%= error_messages_for :email_template if @email_template.errors.any? %>
  4 +
  5 + <div class="template-fields">
  6 + <div class="header-fields">
  7 + <%= labelled_form_field(_('Template Name:'), f.text_field(:name)) %>
  8 + <%= labelled_form_field(_('Template Type:'), f.select(:template_type, @email_template.available_types.map {|k,v| [v[:description], k]}, :include_blank => true)) %>
  9 + <%= labelled_form_field(_('Subject:'), f.text_field(:subject)) %>
  10 + </div>
  11 + <div class="available-params">
  12 + <div class="reference">
  13 + <a target="_blank" href="https://github.com/Shopify/liquid/wiki/Liquid-for-Designers"><%= _('Template language reference') %></a>
  14 + </div>
  15 + <div class="label">
  16 + <%= _('The following parameters may be used in subject and body:') %>
  17 + </div>
  18 + <div class="values">
  19 + <%= @template_params_allowed %>
  20 + </div>
  21 + </div>
  22 + <%= render :file => 'shared/tiny_mce' %>
  23 + <%= labelled_form_field(_('Body:'), f.text_area(:body, :class => 'mceEditor')) %>
  24 + </div>
  25 +
  26 + <div class="actions">
  27 + <%= submit_button(:save, _('Save')) %>
  28 + <%= button(:back, _('Back'), :action => :index) %>
  29 + </div>
  30 +
  31 +<% end %>
... ...
app/views/email_templates/edit.html.erb 0 → 100644
... ... @@ -0,0 +1,3 @@
  1 +<h1>Editing Email Template</h1>
  2 +
  3 +<%= render 'form' %>
... ...
app/views/email_templates/index.html.erb 0 → 100644
... ... @@ -0,0 +1,27 @@
  1 +<div class="email-templates">
  2 + <h1><%= _('Email Templates') %></h1>
  3 +
  4 + <table>
  5 + <tr>
  6 + <th><%= _('Name') %></th>
  7 + <th><%= _('Type') %></th>
  8 + <th><%= _('Actions') %></th>
  9 + </tr>
  10 +
  11 + <% @email_templates.each do |email_template| %>
  12 + <tr>
  13 + <td><%= email_template.name %></td>
  14 + <td><%= email_template.type_description %></td>
  15 + <td>
  16 + <%= button_without_text(:edit, _('Edit'), {:action => :edit, :id => email_template.id}) %>
  17 + <%= button_without_text(:remove, _('Remove'), {:action => :destroy, :id => email_template.id}, method: :delete, data: { confirm: 'Are you sure?' }) %>
  18 + </td>
  19 + </tr>
  20 + <% end %>
  21 + </table>
  22 +
  23 + <br />
  24 +
  25 + <%= button(:new, _('New template'), :action => :new) %>
  26 + <%= button(:back, _('Back'), @back_to) %>
  27 +</div>
... ...
app/views/email_templates/new.html.erb 0 → 100644
... ... @@ -0,0 +1,3 @@
  1 +<h1>New Email Template</h1>
  2 +
  3 +<%= render 'form' %>
... ...
app/views/email_templates/show.html.erb 0 → 100644
... ... @@ -0,0 +1,5 @@
  1 +<p id="notice"><%= notice %></p>
  2 +
  3 +
  4 +<%= link_to 'Edit', url_for(:action => :edit, :id => @email_template.id) %> |
  5 +<%= link_to 'Back', url_for(:action => :index) %>
... ...
app/views/profile/send_mail.html.erb
... ... @@ -6,6 +6,11 @@
6 6  
7 7 <% to = @mailing.data[:members_filtered].present? ? @mailing.recipients.map{|r| r.name}.join(', ') : _('All members')%>
8 8 <%= labelled_form_field(_('To:'), text_area(:data, 'members_filtered', :value => to, :rows => 4, :disabled => 'disabled', :class => 'send-mail-recipients')) %>
  9 +<div class="template-selection">
  10 + <% if @email_templates.present? %>
  11 + <%= 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'))) %>
  12 + </div>
  13 +<% end %>
9 14  
10 15 <%= form_for :mailing, :url => {:action => 'send_mail'}, :html => {:id => 'mailing-form'} do |f| %>
11 16  
... ...
app/views/profile_editor/index.html.erb
... ... @@ -72,6 +72,8 @@
72 72  
73 73 <%= control_panel_button(_('Edit welcome page'), 'welcome-page', :action => 'welcome_page') if has_welcome_page %>
74 74  
  75 + <%= control_panel_button(_('Email Templates'), 'email-templates', :controller => :profile_email_templates) if profile.organization? %>
  76 +
75 77 <% @plugins.dispatch(:control_panel_buttons).each do |button| %>
76 78 <%= control_panel_button(button[:title], button[:icon], button[:url], button[:html_options]) %>
77 79 <% end %>
... ...
app/views/tasks/_approve_article_accept_details.html.erb
  1 +<%= task_email_template(_('Select an acceptance email template:'), @acceptance_email_templates, task) %>
  2 +
1 3 <%= render :file => 'shared/tiny_mce' %>
2 4  
3 5 <%= labelled_form_field(_('Create a link'), f.check_box(:create_link)) %>
... ...
app/views/tasks/_task_reject_details.html.erb
  1 +<%= task_email_template(_('Select a rejection email template:'), @rejection_email_templates, task) %>
  2 +
1 3 <%= labelled_form_field(_('Rejection explanation'), f.text_area(:reject_explanation, :rows => 5)) %>
... ...
db/migrate/20160311184534_create_email_template.rb 0 → 100644
... ... @@ -0,0 +1,12 @@
  1 +class CreateEmailTemplate < ActiveRecord::Migration
  2 + def change
  3 + create_table :email_templates do |t|
  4 + t.string :name
  5 + t.string :template_type
  6 + t.string :subject
  7 + t.text :body
  8 + t.references :owner, :polymorphic => true
  9 + t.timestamps
  10 + end
  11 + end
  12 +end
... ...
db/schema.rb
... ... @@ -357,6 +357,17 @@ ActiveRecord::Schema.define(version: 20160324132518) do
357 357 add_index "domains", ["owner_id", "owner_type", "is_default"], name: "index_domains_on_owner_id_and_owner_type_and_is_default", using: :btree
358 358 add_index "domains", ["owner_id", "owner_type"], name: "index_domains_on_owner_id_and_owner_type", using: :btree
359 359  
  360 + create_table "email_templates", force: :cascade do |t|
  361 + t.string "name"
  362 + t.string "template_type"
  363 + t.string "subject"
  364 + t.text "body"
  365 + t.integer "owner_id"
  366 + t.string "owner_type"
  367 + t.datetime "created_at"
  368 + t.datetime "updated_at"
  369 + end
  370 +
360 371 create_table "environments", force: :cascade do |t|
361 372 t.string "name"
362 373 t.string "contact_email"
... ...
debian/control
... ... @@ -15,6 +15,7 @@ Build-Depends: cucumber,
15 15 ruby-diffy,
16 16 ruby-gettext,
17 17 ruby-launchy-shim,
  18 + ruby-liquid (>= 3.0.3),
18 19 ruby-minitest,
19 20 ruby-minitest-reporters,
20 21 ruby-mocha,
... ...
public/javascripts/application.js
... ... @@ -33,6 +33,7 @@
33 33 *= require require_login.js
34 34 *= require slick.js
35 35 *= require block-store.js
  36 +*= require email_templates.js
36 37 */
37 38  
38 39 // lodash configuration
... ...
public/javascripts/email_templates.js 0 → 100644
... ... @@ -0,0 +1,10 @@
  1 +jQuery(document).ready(function($) {
  2 + $('.template-selection select').change(function() {
  3 + if(!$(this).val()) return;
  4 +
  5 + $.getJSON($(this).data('url'), {id: $(this).val()}, function(data) {
  6 + $('#mailing-form #mailing_subject').val(data.parsed_subject);
  7 + $('#mailing-form .mceEditor').val(data.parsed_body);
  8 + });
  9 + });
  10 +});
... ...
public/javascripts/tasks.js
... ... @@ -2,18 +2,28 @@
2 2  
3 3 $("input.task_accept_radio").click(function(){
4 4 task_id = this.getAttribute("task_id");
5   - $('#on-accept-information-' + task_id).show('fast');
6   - $('#on-reject-information-' + task_id).hide('fast');
  5 + var accept_container = $('#on-accept-information-' + task_id);
  6 + var reject_container = $('#on-reject-information-' + task_id);
  7 +
  8 + accept_container.show('fast');
  9 + reject_container.hide('fast');
7 10 $('#on-skip-information-' + task_id).hide('fast');
8 11 $('#custom-field-information-' + task_id).show('fast');
  12 + reject_container.find('input, select').prop('disabled', true);
  13 + accept_container.find('input, select').prop('disabled', false);
9 14 })
10 15  
11 16 $("input.task_reject_radio").click(function(){
12 17 task_id = this.getAttribute("task_id");
13   - $('#on-accept-information-' + task_id).hide('fast');
14   - $('#on-reject-information-' + task_id).show('fast');
  18 + var accept_container = $('#on-accept-information-' + task_id);
  19 + var reject_container = $('#on-reject-information-' + task_id);
  20 +
  21 + accept_container.hide('fast');
  22 + reject_container.show('fast');
15 23 $('#on-skip-information-' + task_id).hide('fast');
16 24 $('#custom-field-information-' + task_id).show('fast');
  25 + reject_container.find('input, select').prop('disabled', false);
  26 + accept_container.find('input, select').prop('disabled', true);
17 27 })
18 28  
19 29 $("input.task_skip_radio").click(function(){
... ...
public/proposal-app 0 → 160000
... ... @@ -0,0 +1 @@
  1 +Subproject commit e7ad7849b3ef685639d4c5c0bef7b323255e2afa
... ...
public/stylesheets/email-templates.css 0 → 100644
... ... @@ -0,0 +1,18 @@
  1 +.template-fields .header-fields, .template-fields .available-params {
  2 + width: 48%;
  3 + display: inline-block;
  4 +}
  5 +.template-fields .available-params .values {
  6 + color: rgb(111, 111, 111);
  7 +}
  8 +.template-fields .available-params .reference {
  9 + text-align: right;
  10 +}
  11 +.template-fields .available-params .reference a {
  12 + color: rgb(32, 101, 229);
  13 + text-decoration: none;
  14 + font-size: 10px;
  15 +}
  16 +.email-templates table td {
  17 + text-align: center;
  18 +}
... ...
public/stylesheets/profile-editor.scss
... ... @@ -168,6 +168,9 @@
168 168 .controller-profile_editor .msie6 a.control-panel-roles {
169 169 background-image: url(../images/control-panel/role-management.gif)
170 170 }
  171 +.controller-profile_editor .control-panel .control-panel-email-templates {
  172 + background-image: url(../images/control-panel/email.png)
  173 +}
171 174  
172 175 .controller-profile_editor #profile-data {
173 176 display: table;
... ...
test/fixtures/roles.yml
... ... @@ -38,6 +38,7 @@ four:
38 38 - manage_environment_organizations
39 39 - manage_environment_templates
40 40 - manage_environment_licenses
  41 + - manage_email_templates
41 42 profile_admin:
42 43 id: 5
43 44 environment_id: 1
... ... @@ -60,6 +61,7 @@ profile_admin:
60 61 - manage_friends
61 62 - validate_enterprise
62 63 - publish_content
  64 + - manage_email_templates
63 65 profile_member:
64 66 id: 6
65 67 environment_id: 1
... ... @@ -101,3 +103,4 @@ environment_administrator:
101 103 - manage_environment_templates
102 104 - manage_environment_licenses
103 105 - edit_raw_html_block
  106 + - manage_email_templates
... ...
test/functional/environment_email_templates_controller_test.rb 0 → 100644
... ... @@ -0,0 +1,65 @@
  1 +require 'test_helper'
  2 +
  3 +class EnvironmentEmailTemplatesControllerTest < ActionController::TestCase
  4 +
  5 + setup do
  6 + @email_template = EmailTemplate.create!(:name => 'template', :owner => Environment.default)
  7 + person = create_user_with_permission('template_manager', 'manage_email_templates', Environment.default)
  8 + login_as(person.user.login)
  9 + end
  10 +
  11 + test "should get index" do
  12 + get :index
  13 + assert_response :success
  14 + assert_not_nil assigns(:email_templates)
  15 + end
  16 +
  17 + test "should get new" do
  18 + get :new
  19 + assert_response :success
  20 + end
  21 +
  22 + test "should create email_template" do
  23 + assert_difference('EmailTemplate.count') do
  24 + post :create, email_template: { :name => 'test' }
  25 + end
  26 +
  27 + assert_redirected_to url_for(:action => :index)
  28 + end
  29 +
  30 + test "should show email_template" do
  31 + get :show, id: @email_template
  32 + assert_response :success
  33 + end
  34 +
  35 + test "should get edit" do
  36 + get :edit, id: @email_template
  37 + assert_response :success
  38 + end
  39 +
  40 + test "should update email_template" do
  41 + put :update, id: @email_template, email_template: { }
  42 + assert_redirected_to url_for(:action => :index)
  43 + end
  44 +
  45 + test "should destroy email_template" do
  46 + assert_difference('EmailTemplate.count', -1) do
  47 + delete :destroy, id: @email_template
  48 + end
  49 +
  50 + assert_redirected_to url_for(:action => :index)
  51 + end
  52 +
  53 + test "should get parsed template" do
  54 + environment = Environment.default
  55 + @email_template.subject = '{{environment_name}}'
  56 + @email_template.body = '{{environment_name}}'
  57 + @email_template.save!
  58 + get :show_parsed, id: @email_template
  59 + assert_response :success
  60 + json_response = ActiveSupport::JSON.decode(@response.body)
  61 + assert_equal "#{environment.name}", json_response['parsed_subject']
  62 + assert_equal "#{environment.name}", json_response['parsed_body']
  63 + end
  64 +
  65 +end
... ...
test/functional/profile_controller_test.rb
... ... @@ -1527,6 +1527,29 @@ class ProfileControllerTest &lt; ActionController::TestCase
1527 1527 assert_redirected_to :action => 'members'
1528 1528 end
1529 1529  
  1530 + should 'display email templates as an option to send mail' do
  1531 + community = fast_create(Community)
  1532 + create_user_with_permission('profile_moderator_user', 'send_mail_to_members', community)
  1533 + login_as('profile_moderator_user')
  1534 +
  1535 + template1 = EmailTemplate.create!(:owner => community, :name => "Template 1", :template_type => :organization_members)
  1536 + template2 = EmailTemplate.create!(:owner => community, :name => "Template 2")
  1537 +
  1538 + get :send_mail, :profile => community.identifier, :mailing => {:subject => 'Hello', :body => 'We have some news'}
  1539 + assert_select '.template-selection'
  1540 + assert_equal [template1], assigns(:email_templates)
  1541 + end
  1542 +
  1543 + should 'do not display email template selection when there is no template for organization members' do
  1544 + community = fast_create(Community)
  1545 + create_user_with_permission('profile_moderator_user', 'send_mail_to_members', community)
  1546 + login_as('profile_moderator_user')
  1547 +
  1548 + get :send_mail, :profile => community.identifier, :mailing => {:subject => 'Hello', :body => 'We have some news'}
  1549 + assert_select '.template-selection'
  1550 + assert assigns(:email_templates).empty?
  1551 + end
  1552 +
1530 1553 should 'show all fields to anonymous user' do
1531 1554 viewed = create_user('person_1').person
1532 1555 Environment.any_instance.stubs(:active_person_fields).returns(['sex', 'birth_date'])
... ...
test/functional/profile_editor_controller_test.rb
... ... @@ -623,6 +623,17 @@ class ProfileEditorControllerTest &lt; ActionController::TestCase
623 623 assert_tag :tag => 'a', :attributes => { :href => '/myprofile/default_user/cms' }
624 624 end
625 625  
  626 + should 'display email template link for organizations in control panel' do
  627 + profile = fast_create(Organization)
  628 + get :index, :profile => profile.identifier
  629 + assert_tag :tag => 'a', :attributes => { :href => "/myprofile/#{profile.identifier}/profile_email_templates" }
  630 + end
  631 +
  632 + should 'not display email template link in control panel for person' do
  633 + get :index, :profile => profile.identifier
  634 + assert_no_tag :tag => 'a', :attributes => { :href => "/myprofile/#{profile.identifier}/email_templates" }
  635 + end
  636 +
626 637 should 'offer to create blog in control panel' do
627 638 get :index, :profile => profile.identifier
628 639 assert_tag :tag => 'a', :attributes => { :href => "/myprofile/default_user/cms/new?type=Blog" }
... ...
test/functional/profile_email_templates_controller_test.rb 0 → 100644
... ... @@ -0,0 +1,68 @@
  1 +require 'test_helper'
  2 +
  3 +class ProfileEmailTemplatesControllerTest < ActionController::TestCase
  4 +
  5 + setup do
  6 + @profile = fast_create(Community)
  7 + @person = create_user_with_permission('templatemanager', 'manage_email_templates', @profile)
  8 + @email_template = EmailTemplate.create!(:name => 'template', :owner => @profile)
  9 + login_as(@person.user.login)
  10 + end
  11 +
  12 + attr_accessor :profile, :person
  13 +
  14 + test "should get index" do
  15 + get :index, :profile => profile.identifier
  16 + assert_response :success
  17 + assert_not_nil assigns(:email_templates)
  18 + end
  19 +
  20 + test "should get new" do
  21 + get :new, :profile => profile.identifier
  22 + assert_response :success
  23 + end
  24 +
  25 + test "should create email_template" do
  26 + assert_difference('EmailTemplate.count') do
  27 + post :create, email_template: { :name => 'test' }, :profile => profile.identifier
  28 + end
  29 +
  30 + assert_redirected_to url_for(:action => :index)
  31 + end
  32 +
  33 + test "should show email_template" do
  34 + get :show, id: @email_template, :profile => profile.identifier
  35 + assert_response :success
  36 + end
  37 +
  38 + test "should get edit" do
  39 + get :edit, id: @email_template, :profile => profile.identifier
  40 + assert_response :success
  41 + end
  42 +
  43 + test "should update email_template" do
  44 + put :update, id: @email_template, email_template: { }, :profile => profile.identifier
  45 + assert_redirected_to url_for(:action => :index)
  46 + end
  47 +
  48 + test "should destroy email_template" do
  49 + assert_difference('EmailTemplate.count', -1) do
  50 + delete :destroy, id: @email_template, :profile => profile.identifier
  51 + end
  52 +
  53 + assert_redirected_to url_for(:action => :index)
  54 + end
  55 +
  56 + test "should get parsed template" do
  57 + environment = Environment.default
  58 + @email_template.subject = '{{profile_name}} - {{environment_name}}'
  59 + @email_template.body = '{{profile_name}} - {{environment_name}}'
  60 + @email_template.save!
  61 + get :show_parsed, id: @email_template, :profile => profile.identifier
  62 + assert_response :success
  63 + json_response = ActiveSupport::JSON.decode(@response.body)
  64 + assert_equal "#{@person.name} - #{environment.name}", json_response['parsed_subject']
  65 + assert_equal "#{@person.name} - #{environment.name}", json_response['parsed_body']
  66 + end
  67 +
  68 +end
... ...
test/functional/tasks_controller_test.rb
... ... @@ -704,4 +704,30 @@ class TasksControllerTest &lt; ActionController::TestCase
704 704 assert_tag :tag=> 'div', :attributes => { :class => 'field-name' }, :content => /great_field: new value for community!/
705 705 end
706 706  
  707 + should "display email template selection when accept a task" do
  708 + community = fast_create(Community)
  709 + @controller.stubs(:profile).returns(community)
  710 + person = create_user_with_permission('taskviewer', 'view_tasks', community)
  711 + login_as person.user.login
  712 +
  713 + email_template = EmailTemplate.create!(:name => 'template', :owner => community, :template_type => :task_acceptance)
  714 + task = ApproveArticle.create!(:requestor => person, :target => community, :responsible => person)
  715 + get :index
  716 + assert_select "#on-accept-information-#{task.id} .template-selection"
  717 + assert_equal [email_template], assigns(:acceptance_email_templates)
  718 + end
  719 +
  720 + should "display email template selection when reject a task" do
  721 + community = fast_create(Community)
  722 + @controller.stubs(:profile).returns(community)
  723 + person = create_user_with_permission('taskviewer', 'view_tasks', community)
  724 + login_as person.user.login
  725 +
  726 + email_template = EmailTemplate.create!(:name => 'template', :owner => community, :template_type => :task_rejection)
  727 + task = ApproveArticle.create!(:requestor => person, :target => community, :responsible => person)
  728 + get :index
  729 + assert_select "#on-reject-information-#{task.id} .template-selection"
  730 + assert_equal [email_template], assigns(:rejection_email_templates)
  731 + end
  732 +
707 733 end
... ...
test/unit/change_password_test.rb
... ... @@ -71,4 +71,10 @@ class ChangePasswordTest &lt; ActiveSupport::TestCase
71 71 assert_match(/#{task.requestor.name} wants to change its password/, email.subject)
72 72 end
73 73  
  74 + should 'set email template when it exists' do
  75 + template = EmailTemplate.create!(:template_type => :user_change_password, :name => 'template1', :owner => Environment.default)
  76 + task = ChangePassword.create!(:requestor => person)
  77 + assert_equal template.id, task.email_template_id
  78 + end
  79 +
74 80 end
... ...
test/unit/email_template_helper_test.rb 0 → 100644
... ... @@ -0,0 +1,20 @@
  1 +require_relative "../test_helper"
  2 +
  3 +class EmailTemplateHelperTest < ActionView::TestCase
  4 +
  5 + should 'replace body and subject with parsed values from template' do
  6 + template = mock
  7 + template.expects(:parsed_body).returns('parsed body')
  8 + template.expects(:parsed_subject).returns('parsed subject')
  9 + params = {:subject => 'subject', :body => 'body', :email_template => template}
  10 + expects(:mail).with({:subject => 'parsed subject', :body => 'parsed body', :content_type => 'text/html'})
  11 + mail_with_template(params)
  12 + end
  13 +
  14 + should 'do not change params if there is no email template' do
  15 + params = {:subject => 'subject', :body => 'body'}
  16 + expects(:mail).with(params)
  17 + mail_with_template(params)
  18 + end
  19 +
  20 +end
... ...
test/unit/email_template_test.rb 0 → 100644
... ... @@ -0,0 +1,53 @@
  1 +require_relative "../test_helper"
  2 +
  3 +class EmailTemplateTest < ActiveSupport::TestCase
  4 +
  5 + should 'filter templates by type' do
  6 + EmailTemplate.create!(:template_type => :type1, :name => 'template1')
  7 + EmailTemplate.create!(:template_type => :type2, :name => 'template2')
  8 + EmailTemplate.create!(:template_type => :type2, :name => 'template3')
  9 + assert_equal ['template2', 'template3'], EmailTemplate.find_all_by_template_type(:type2).map(&:name)
  10 + end
  11 +
  12 + should 'parse body using params' do
  13 + template = EmailTemplate.new(:body => 'Hi {{person}}')
  14 + assert_equal 'Hi John', template.parsed_body({:person => 'John'})
  15 + end
  16 +
  17 + should 'parse subject using params' do
  18 + template = EmailTemplate.new(:subject => 'Hi {{person}}')
  19 + assert_equal 'Hi John', template.parsed_subject({:person => 'John'})
  20 + end
  21 +
  22 + should 'not create template with the same name of other' do
  23 + template1 = EmailTemplate.new(:template_type => :type1, :name => 'template', :owner => Environment.default)
  24 + template2 = EmailTemplate.new(:template_type => :type1, :name => 'template', :owner => Environment.default)
  25 + assert template1.save
  26 + assert !template2.save
  27 + end
  28 +
  29 + should 'not create duplicated template when template type is unique' do
  30 + template1 = EmailTemplate.new(:template_type => :user_activation, :name => 'template1', :owner => Environment.default)
  31 + template2 = EmailTemplate.new(:template_type => :user_activation, :name => 'template2', :owner => Environment.default)
  32 + assert template1.save
  33 + assert !template2.save
  34 + end
  35 +
  36 + should 'create duplicated template when template type is not unique' do
  37 + template1 = EmailTemplate.new(:template_type => :task_rejection, :name => 'template1', :owner => Environment.default)
  38 + template2 = EmailTemplate.new(:template_type => :task_rejection, :name => 'template2', :owner => Environment.default)
  39 + assert template1.save
  40 + assert template2.save
  41 + end
  42 +
  43 + should 'return available types when the owner is an environment' do
  44 + template = EmailTemplate.new(:owner => Environment.default)
  45 + assert_equal [:user_activation, :user_change_password], template.available_types.symbolize_keys.keys
  46 + end
  47 +
  48 + should 'return available types when the owner is a profile' do
  49 + template = EmailTemplate.new(:owner => Profile.new)
  50 + assert_equal [:task_rejection, :task_acceptance, :organization_members], template.available_types.symbolize_keys.keys
  51 + end
  52 +
  53 +end
... ...
test/unit/task_helper_test.rb 0 → 100644
... ... @@ -0,0 +1,23 @@
  1 +require_relative "../test_helper"
  2 +
  3 +class TaskHelperTest < ActionView::TestCase
  4 +
  5 + include ApplicationHelper
  6 +
  7 + def setup
  8 + @profile = fast_create(Profile)
  9 + @task = fast_create(Task, :target_id => @profile.id)
  10 + end
  11 +
  12 + attr_accessor :task, :profile
  13 +
  14 + should 'return select field for template selection when there is templates to choose' do
  15 + email_templates = 3.times.map { EmailTemplate.new }
  16 + assert_tag_in_string task_email_template('Description', email_templates, task), :tag => 'div', :attributes => {:class => 'template-selection'}
  17 + end
  18 +
  19 + should 'not return select field for template selection when there is no templates to choose' do
  20 + assert task_email_template('Description', [], task).blank?
  21 + end
  22 +
  23 +end
... ...
test/unit/task_mailer_test.rb
... ... @@ -123,6 +123,53 @@ class TaskMailerTest &lt; ActiveSupport::TestCase
123 123 assert_match(/#{url_to_compare}/, mail.body.to_s)
124 124 end
125 125  
  126 + should 'be able to send rejection notification based on a selected template' do
  127 + task = Task.new
  128 + task.expects(:task_cancelled_message).returns('the message')
  129 + task.reject_explanation = 'explanation'
  130 +
  131 + profile = fast_create(Community)
  132 + 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}}')
  133 + task.email_template_id = email_template.id
  134 +
  135 + requestor = Profile.new(:name => 'my name')
  136 + requestor.expects(:notification_emails).returns(['requestor@example.com']).at_least_once
  137 +
  138 + task.expects(:requestor).returns(requestor).at_least_once
  139 + requestor.expects(:environment).returns(@environment).at_least_once
  140 + task.expects(:environment).returns(@environment).at_least_once
  141 +
  142 + task.send(:send_notification, :cancelled).deliver
  143 + assert !ActionMailer::Base.deliveries.empty?
  144 + mail = ActionMailer::Base.deliveries.last
  145 + assert_match /text\/html/, mail.content_type
  146 + assert_equal 'template subject - example', mail.subject.to_s
  147 + assert_equal 'template body - example - my name - explanation', mail.body.to_s
  148 + end
  149 +
  150 + should 'be able to send accept notification based on a selected template' do
  151 + task = Task.new
  152 + task.expects(:task_finished_message).returns('the message')
  153 +
  154 + profile = fast_create(Community)
  155 + email_template = EmailTemplate.create!(:owner => profile, :name => 'Template 1', :subject => 'template subject - {{environment.name}}', :body => 'template body - {{environment.name}} - {{task.requestor.name}}')
  156 + task.email_template_id = email_template.id
  157 +
  158 + requestor = Profile.new(:name => 'my name')
  159 + requestor.expects(:notification_emails).returns(['requestor@example.com']).at_least_once
  160 +
  161 + task.expects(:requestor).returns(requestor).at_least_once
  162 + requestor.expects(:environment).returns(@environment).at_least_once
  163 + task.expects(:environment).returns(@environment).at_least_once
  164 +
  165 + task.send(:send_notification, :finished).deliver
  166 + assert !ActionMailer::Base.deliveries.empty?
  167 + mail = ActionMailer::Base.deliveries.last
  168 + assert_match /text\/html/, mail.content_type
  169 + assert_equal 'template subject - example', mail.subject.to_s
  170 + assert_equal 'template body - example - my name', mail.body.to_s
  171 + end
  172 +
126 173 private
127 174 def read_fixture(action)
128 175 IO.readlines("#{FIXTURES_PATH}/task_mailer/#{action}")
... ...
test/unit/user_mailer_test.rb
... ... @@ -26,6 +26,24 @@ fast_create(Person))
26 26 assert_match /profile\/some-user\/friends\/suggest/, email.body.to_s
27 27 end
28 28  
  29 + should 'deliver activation code email' do
  30 + assert_difference 'ActionMailer::Base.deliveries.size' do
  31 + u = create_user('some-user')
  32 + UserMailer.activation_code(u).deliver
  33 + end
  34 + end
  35 +
  36 + should 'deliver activation code email with template' do
  37 + EmailTemplate.create!(:template_type => :user_activation, :name => 'template1', :subject => 'activation template subject', :body => 'activation template body', :owner => Environment.default)
  38 + assert_difference 'ActionMailer::Base.deliveries.size' do
  39 + u = create_user('some-user')
  40 + UserMailer.activation_code(u).deliver
  41 + end
  42 + mail = ActionMailer::Base.deliveries.last
  43 + assert_equal 'activation template subject', mail.subject.to_s
  44 + assert_equal 'activation template body', mail.body.to_s
  45 + end
  46 +
29 47 private
30 48  
31 49 def read_fixture(action)
... ...