From 63871baf6061668b162972193c55b5a8f7490797 Mon Sep 17 00:00:00 2001 From: elijah Date: Thu, 30 Apr 2015 00:32:33 -0700 Subject: added support for email notifications of ticket changes --- engines/support/Gemfile | 6 -- engines/support/Gemfile.lock | 14 ++++ engines/support/Rakefile | 12 +--- .../support/app/controllers/tickets_controller.rb | 8 +++ engines/support/app/helpers/ticket_i18n_helper.rb | 20 ++++++ engines/support/app/mailers/.gitkeep | 0 engines/support/app/mailers/ticket_mailer.rb | 82 ++++++++++++++++++++++ engines/support/app/models/ticket.rb | 43 ++++++++---- engines/support/app/models/ticket_comment.rb | 40 ++++------- .../app/views/ticket_mailer/notice.text.erb | 5 ++ engines/support/config/locales/en.yml | 8 ++- engines/support/config/locales/es.yml | 3 + engines/support/leap_web_help.gemspec | 6 +- engines/support/test/factories.rb | 2 +- .../support/test/functional/ticket_mailer_test.rb | 7 ++ engines/support/test/unit/ticket_test.rb | 2 +- 16 files changed, 193 insertions(+), 65 deletions(-) create mode 100644 engines/support/Gemfile.lock create mode 100644 engines/support/app/helpers/ticket_i18n_helper.rb create mode 100644 engines/support/app/mailers/.gitkeep create mode 100644 engines/support/app/mailers/ticket_mailer.rb create mode 100644 engines/support/app/views/ticket_mailer/notice.text.erb create mode 100644 engines/support/config/locales/es.yml create mode 100644 engines/support/test/functional/ticket_mailer_test.rb (limited to 'engines') diff --git a/engines/support/Gemfile b/engines/support/Gemfile index ad7d29b..0296338 100644 --- a/engines/support/Gemfile +++ b/engines/support/Gemfile @@ -1,11 +1,5 @@ source "https://rubygems.org" -eval(File.read(File.dirname(__FILE__) + '/../common_dependencies.rb')) -eval(File.read(File.dirname(__FILE__) + '/..//ui_dependencies.rb')) - -# We require leap_web_core from here so we can use the path option. -gem "leap_web_core", :path => '../core' - # Declare your gem's dependencies in leap_web_users.gemspec. # Bundler will treat runtime dependencies like base dependencies, and # development dependencies will be added by default to the :development group. diff --git a/engines/support/Gemfile.lock b/engines/support/Gemfile.lock new file mode 100644 index 0000000..fe4ce6c --- /dev/null +++ b/engines/support/Gemfile.lock @@ -0,0 +1,14 @@ +PATH + remote: . + specs: + leap_web_help (0.6.0) + +GEM + remote: https://rubygems.org/ + specs: + +PLATFORMS + ruby + +DEPENDENCIES + leap_web_help! diff --git a/engines/support/Rakefile b/engines/support/Rakefile index 0e73163..4787e78 100644 --- a/engines/support/Rakefile +++ b/engines/support/Rakefile @@ -1,8 +1,4 @@ #!/usr/bin/env rake - -require 'rake/packagetask' -require 'rubygems/package_task' - begin require 'bundler/setup' rescue LoadError @@ -18,16 +14,14 @@ end RDoc::Task.new(:rdoc) do |rdoc| rdoc.rdoc_dir = 'rdoc' - rdoc.title = 'LeapWebHelp' + rdoc.title = 'Blah' rdoc.options << '--line-numbers' rdoc.rdoc_files.include('README.rdoc') rdoc.rdoc_files.include('lib/**/*.rb') end -spec = eval(File.read('leap_web_help.gemspec')) -Gem::PackageTask.new(spec) do |p| - p.gem_spec = spec -end + + Bundler::GemHelper.install_tasks diff --git a/engines/support/app/controllers/tickets_controller.rb b/engines/support/app/controllers/tickets_controller.rb index 602bbd9..c1abfa2 100644 --- a/engines/support/app/controllers/tickets_controller.rb +++ b/engines/support/app/controllers/tickets_controller.rb @@ -62,6 +62,7 @@ class TicketsController < ApplicationController if @ticket.comments_changed? @ticket.comments.last.posted_by = current_user.id @ticket.comments.last.private = false unless admin? + send_email_update(@ticket, @ticket.comments.last) end flash_for @ticket, with_errors: true @@ -153,4 +154,11 @@ class TicketsController < ApplicationController ) end + def send_email_update(ticket, comment) + TicketMailer.send_notice(ticket, comment, ticket_url(ticket)) + rescue StandardError => exc + flash_for(exc) + raise exc if Rails.env == 'development' + end + end diff --git a/engines/support/app/helpers/ticket_i18n_helper.rb b/engines/support/app/helpers/ticket_i18n_helper.rb new file mode 100644 index 0000000..61b4cf2 --- /dev/null +++ b/engines/support/app/helpers/ticket_i18n_helper.rb @@ -0,0 +1,20 @@ +module TicketI18nHelper + + # + # outputs translations for all the possible translated strings. + # used in emails, sense we don't know the locale of the recipient. + # + def t_all_available(key) + default = I18n.t(key, locale: I18n.default_locale) + result = [] + result << "[#{I18n.default_locale}] #{default}" + I18n.available_locales.each do |locale| + text = I18n.t(key, locale: locale, default: default) + if text != default + result << "[#{locale}] #{text}" + end + end + result.join("\n") + end + +end diff --git a/engines/support/app/mailers/.gitkeep b/engines/support/app/mailers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/engines/support/app/mailers/ticket_mailer.rb b/engines/support/app/mailers/ticket_mailer.rb new file mode 100644 index 0000000..9a83345 --- /dev/null +++ b/engines/support/app/mailers/ticket_mailer.rb @@ -0,0 +1,82 @@ +class TicketMailer < ActionMailer::Base + + helper :ticket_i18n + + def self.send_notice(ticket, comment, url) + reply_recipients(ticket, comment.user).each do |email, key| + TicketMailer.notice(ticket, comment, url, email, key).deliver + end + end + + # + # ticket - the ticket in question + # author - user who created last comment + # url - url of the ticket + # email - email to send reply notice to + # key - public key for email + # + # TODO: OpenPGP encrypt email to the public key. + # + def notice(ticket, comment, url, email, key) + @url = url + @email = email + @key = key + mail({ + :reply_to => reply_to_address, + :subject => "Re: [#{ticket.id}] #{ticket.subject}", + :to => email, + :from => from_address(ticket, comment) + }) + end + + private + + # + # I am not sure what makes the most sense here. For now, since we do not + # include any reply text in the notification email, it makes sense to make + # the from address be the robot, not the user. + # + def from_address(ticket, comment) + if true + reply_to_address + else + from_name = if comment.user.present? + comment.user.login + elsif ticket.created_by.present? + ticket.created_by_user.login + else + I18n.t(:anonymous) + end + "%s <%s>" % [from_name, reply_to_address] + end + end + + # TODO: change me to support virtual domains + def reply_to_address + [APP_CONFIG[:mailer][:from_address], APP_CONFIG[:domain]].join('@') + end + + # + # returns a hash of {'email' => 'public key'}, where key might be nil. + # + def self.reply_recipients(ticket, author) + recipients = {} + ticket.comments.each do |comment| + user = comment.posted_by_user + if user && (author.nil? || user.id != author.id) + if user.email + recipients[user.identity.address] = (user.identity.keys[:pgp] if user.identity.keys[:pgp].present?) + end + if user.contact_email.present? + recipients[user.contact_email] = (user.contact_email_key if user.contact_email_key.present?) + end + end + end + if author && author.is_admin? && ticket.email.present? + recipients[ticket.email] = nil + end + logger.info { "emailing reply regarding ticket #{ticket.id} to #{recipients.keys.join(', ')}" } + recipients + end + +end diff --git a/engines/support/app/models/ticket.rb b/engines/support/app/models/ticket.rb index 554fbd6..b1bdf8d 100644 --- a/engines/support/app/models/ticket.rb +++ b/engines/support/app/models/ticket.rb @@ -3,10 +3,10 @@ # look into whether that should be tweaked, and whether it works okay with # pagination (seems to now...) # -# TODO: better validation of email -# # TODO: don't hardcode strings 'unknown user' and 'unauthenticated user' # +# TODO: this should use associations instead of non-standard created_by. +# class Ticket < CouchRest::Model::Base use_database "tickets" @@ -19,6 +19,8 @@ class Ticket < CouchRest::Model::Base timestamps! + unique_id :generate_code + design do view :by_updated_at view :by_created_at @@ -33,13 +35,11 @@ class Ticket < CouchRest::Model::Base validates :subject, :presence => true - # email can have three states: - # * nil - prefilled with created_by's email - # * "" - cleared - # * valid email address - validates :email, :allow_blank => true, :format => /\A(([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,}))?\Z/ - - # validates :comments, presence: true + # email can be nil, "", or valid address. + # validation provided by 'valid_email' gem. + validates :email, :allow_blank => true, + :email => true, + :mx_with_fallback => true def self.search(options = {}) @selection = TicketSelection.new(options) @@ -74,17 +74,21 @@ class Ticket < CouchRest::Model::Base self.is_open = true end - def commenters + def commenters(locale = I18n.locale) commenters = [] + unknown = false + anonymous = false self.comments.each do |comment| if comment.posted_by if user = User.find(comment.posted_by) commenters << user.login if user and !commenters.include?(user.login) - else - commenters << 'unknown user' if !commenters.include?('unknown user') + elsif !unknown + unknown = true + commenters << I18n.t(:unknown, :locale => locale) end - else - commenters << 'unauthenticated user' if !commenters.include?('unauthenticated user') + elsif !anonymous + anonymous = true + commenters << I18n.t(:anonymous, :locale => locale) end end commenters.join(', ') @@ -113,4 +117,15 @@ class Ticket < CouchRest::Model::Base User.find_by_login(self.regarding_user) end + # Generate a unique code for identifying this ticket. + # This will become the ID of the document. It is also used for + # tracking email replies. The code must be URL friendly. + def generate_code + while code = SecureRandom.urlsafe_base64.downcase.gsub(/[li1o0_-]/,'')[0..7] + if code.length == 8 && self.class.find(code).nil? + return code + end + end + end + end diff --git a/engines/support/app/models/ticket_comment.rb b/engines/support/app/models/ticket_comment.rb index 2c5df41..1c04a1f 100644 --- a/engines/support/app/models/ticket_comment.rb +++ b/engines/support/app/models/ticket_comment.rb @@ -1,22 +1,17 @@ +# +# elijah: the property 'posted_by' should have been a belongs_to() association +# or at least 'posted_by_id', but I am leaving it as is because I am +# not sure how to change it. +# class TicketComment include CouchRest::Model::Embeddable - #belongs_to :ticket #is this best way to do it? will want to access all of a tickets comments, so maybe this isn't the way? - property :posted_by, String#, :protected => true #Integer#this should be current_user if that is set, meaning the user is logged in #cannot have it be protected and set via comments_attributes=. also, if it is protected and we set in the tickets_controller, it gets unset. TODO---is this okay to have it not protected and manually check it? We do not users to be able to set this. - # if the current user is not set, then we could just say the comment comes from an 'unauthenticated user', which would be somebody with the secret URL - property :posted_at, Time#, :protected => true - #property :posted_verified, TrueClass, :protected => true #should be true if current_user is set when the comment is created + property :posted_by, String + property :posted_at, Time property :body, String - property :private, TrueClass # private comments are only viewable by admins #this is checked when set, to make sure it was set by an admin + property :private, TrueClass - # ? timestamps! validates :body, :presence => true - #before_validation :set_time#, :set_posted_by - - #design do - # view :by_posted_at - # view :by_body - #end # translations are in the same scope as those of a "proper" couchrest model def self.i18n_scope @@ -28,21 +23,10 @@ class TicketComment end def posted_by_user - User.find(posted_by) if posted_by - end - -=begin - #TODO. - #this is resetting all comments associated with the ticket: - def set_time - self.posted_at = Time.now - end -=end - -=begin - def set_posted_by - self.posted_by = User.current if User.current + if posted_by + @_posted_by_user ||= User.find(posted_by) + end end -=end + alias user posted_by_user end diff --git a/engines/support/app/views/ticket_mailer/notice.text.erb b/engines/support/app/views/ticket_mailer/notice.text.erb new file mode 100644 index 0000000..c0e6f44 --- /dev/null +++ b/engines/support/app/views/ticket_mailer/notice.text.erb @@ -0,0 +1,5 @@ +<%= t_all_available(:email_notice_text) %> + + <%= @url %> + +<%= t_all_available(:email_no_reply_text) %> \ No newline at end of file diff --git a/engines/support/config/locales/en.yml b/engines/support/config/locales/en.yml index 8d2af67..071db97 100644 --- a/engines/support/config/locales/en.yml +++ b/engines/support/config/locales/en.yml @@ -1,9 +1,11 @@ en: support_tickets: "Support" + email_notice_text: "A new comment has been added to this help ticket." + email_no_reply_text: "Do not reply to this email." # translations used in the layout views or @title layouts: # fallback for all translations of "tickets" nested below: - tickets: "Tickets" + tickets: "Tickets" title: tickets: "Tickets" header: @@ -90,8 +92,8 @@ en: "false": "Closed" # mouse over hints for the given fields hints: - ticket: - email: "Please provide an email address so we can get back to you." + ticket: + email: "Provide an email address in order to notified when this ticket is updated." # these will fallback to translations in "simple_form.hints.defaults" # placeholders inside the fields before anything was typed #placeholders: diff --git a/engines/support/config/locales/es.yml b/engines/support/config/locales/es.yml new file mode 100644 index 0000000..7773197 --- /dev/null +++ b/engines/support/config/locales/es.yml @@ -0,0 +1,3 @@ +es: + email_notice_text: "Se ha aƱadido un nuevo comentario." + email_no_reply_text: "No responda a este mensaje." \ No newline at end of file diff --git a/engines/support/leap_web_help.gemspec b/engines/support/leap_web_help.gemspec index 7b668d5..818604f 100644 --- a/engines/support/leap_web_help.gemspec +++ b/engines/support/leap_web_help.gemspec @@ -8,9 +8,9 @@ Gem::Specification.new do |s| s.version = LeapWeb::VERSION s.authors = ["Jessib"] s.email = ["jessib@leap.se"] - s.homepage = "http://www.leap.se" - s.summary = "Help Desk for LeapWeb" - s.description = "Managing Tickets for a Leap provider" + s.homepage = "https://www.leap.se" + s.summary = "Help Desk for LEAP webapp" + s.description = "Managing help tickets for a LEAP provider" s.files = Dir["{app,config,lib}/**/*"] + ["Rakefile", "README.md"] s.test_files = Dir["test/**/*"] diff --git a/engines/support/test/factories.rb b/engines/support/test/factories.rb index bcf41e8..3fc28b3 100644 --- a/engines/support/test/factories.rb +++ b/engines/support/test/factories.rb @@ -2,7 +2,7 @@ FactoryGirl.define do factory :ticket do subject { Faker::Lorem.sentence } - email { Faker::Internet.email } + email { "fake@example.org" } factory :ticket_with_comment do comments_attributes do diff --git a/engines/support/test/functional/ticket_mailer_test.rb b/engines/support/test/functional/ticket_mailer_test.rb new file mode 100644 index 0000000..6bbaaaf --- /dev/null +++ b/engines/support/test/functional/ticket_mailer_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class TicketMailerTest < ActionMailer::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/engines/support/test/unit/ticket_test.rb b/engines/support/test/unit/ticket_test.rb index 678d8dc..c64e8f4 100644 --- a/engines/support/test/unit/ticket_test.rb +++ b/engines/support/test/unit/ticket_test.rb @@ -13,7 +13,7 @@ class TicketTest < ActiveSupport::TestCase end test "ticket validates email format" do - t = FactoryGirl.build :ticket, email: "aswerssfd" + t = FactoryGirl.build :ticket, email: "invalid email" assert !t.valid? end -- cgit v1.2.3