diff --git a/app/helpers/person_notifier_helper.rb b/app/helpers/person_notifier_helper.rb new file mode 100644 index 0000000..65b084c --- /dev/null +++ b/app/helpers/person_notifier_helper.rb @@ -0,0 +1,15 @@ +module PersonNotifierHelper + + include ApplicationHelper + + private + + def path_to_image(source) + top_url + source + end + + def top_url + top_url = @profile.environment ? @profile.environment.top_url : '' + end + +end diff --git a/app/models/person.rb b/app/models/person.rb index ba9d0f0..544e0f0 100644 --- a/app/models/person.rb +++ b/app/models/person.rb @@ -491,6 +491,17 @@ class Person < Profile gravatar_profile_image_url(self.email, :size=>20, :d => gravatar_default) end + settings_items :last_notification, :type => DateTime + settings_items :notification_time, :type => :integer, :default => 0 + + def notifier + @notifier ||= PersonNotifier.new(self) + end + + after_update do |person| + person.notifier.reschedule_next_notification_mail + end + protected def followed_by?(profile) diff --git a/app/models/person_notifier.rb b/app/models/person_notifier.rb new file mode 100644 index 0000000..a3d3263 --- /dev/null +++ b/app/models/person_notifier.rb @@ -0,0 +1,87 @@ +class PersonNotifier + + def initialize(person) + @person = person + end + + def self.schedule_all_next_notification_mail + Delayed::Job.enqueue(NotifyAllJob.new) unless NotifyAllJob.exists? + end + + def schedule_next_notification_mail + dispatch_notification_mail if !NotifyJob.exists?(@person.id) + end + + def dispatch_notification_mail + Delayed::Job.enqueue(NotifyJob.new(@person.id), nil, @person.notification_time.hours.from_now) if @person.notification_time>0 + end + + def reschedule_next_notification_mail + return nil unless @person.setting_changed?(:notification_time) || @person.setting_changed?(:last_notification) + NotifyJob.find(@person.id).delete_all + schedule_next_notification_mail + end + + def notify + if @person.notification_time && @person.notification_time > 0 + from = @person.last_notification || DateTime.now - @person.notification_time.hours + notifications = @person.tracked_notifications.find(:all, :conditions => ["created_at > ?", from]) + Mailer::deliver_content_summary(@person, notifications) unless notifications.empty? + @person.settings[:last_notification] = DateTime.now + @person.save! + end + end + + class NotifyAllJob + def self.exists? + Delayed::Job.where(:handler => "--- !ruby/object:PersonNotifier::NotifyAllJob {}\n\n").count > 0 + end + + def perform + Person.find_each {|person| person.notifier.schedule_next_notification_mail } + end + end + + class NotifyJob < Struct.new(:person_id) + + def self.exists?(person_id) + !find(person_id).empty? + end + + def self.find(person_id) + Delayed::Job.where(:handler => "--- !ruby/struct:PersonNotifier::NotifyJob \nperson_id: #{person_id}\n") + end + + def perform + Person.find(person_id).notifier.notify + end + + def on_permanent_failure + person = Person.find(person_id) + person.notifier.dispatch_notification_mail + end + + end + + class Mailer < ActionMailer::Base + + add_template_helper(PersonNotifierHelper) + + def session + {:theme => nil} + end + + def content_summary(person, notifications) + @current_theme = 'default' + @profile = person + recipients person.email + from "#{@profile.environment.name} <#{@profile.environment.contact_email}>" + subject _("[%s] Network Activity") % [@profile.environment.name] + body :recipient => @profile.nickname || @profile.name, + :environment => @profile.environment.name, + :url => @profile.environment.top_url, + :notifications => notifications + content_type "text/html" + end + end +end diff --git a/app/views/person_notifier/mailer/_add_member_in_community.rhtml b/app/views/person_notifier/mailer/_add_member_in_community.rhtml new file mode 100644 index 0000000..ce1d787 --- /dev/null +++ b/app/views/person_notifier/mailer/_add_member_in_community.rhtml @@ -0,0 +1 @@ +<%= render :partial => 'default_activity', :locals => { :activity => activity } %> diff --git a/app/views/person_notifier/mailer/_comment.rhtml b/app/views/person_notifier/mailer/_comment.rhtml new file mode 100644 index 0000000..b0430a0 --- /dev/null +++ b/app/views/person_notifier/mailer/_comment.rhtml @@ -0,0 +1,33 @@ +<% Comment %> +<% Profile %> +<% Person %> + + + + + +
+ <% if comment.author %> + <%= link_to profile_image(comment.author, :minor), + comment.author_url, + :class => 'comment-picture', + :title => comment.author_name + %> + <% end %> + + <%= comment.author.present? ? link_to(comment.author_name, comment.author.url, :style => "font-size: 12px; color: #333; font-weight: bold; text-decoration: none;") : content_tag('strong', comment.author_name) %> + <% unless comment.title.blank? %> + <%= comment.title %>
+ <% end %> + <%= txt2html comment.body %>
+ <%= time_ago_as_sentence(comment.created_at) %> +
+ + <% unless comment.replies.blank? %> +
    + <% comment.replies.each do |reply| %> + <%= render :partial => 'comment', :locals => { :comment => reply } %> + <% end %> +
+ <% end %> +
diff --git a/app/views/person_notifier/mailer/_create_article.rhtml b/app/views/person_notifier/mailer/_create_article.rhtml new file mode 100644 index 0000000..c7100ee --- /dev/null +++ b/app/views/person_notifier/mailer/_create_article.rhtml @@ -0,0 +1,27 @@ + + + + + + + + + +
+ <%= link_to(profile_image(activity.user, :minor), activity.user.url) %> + +

+ <%= link_to activity.user.short_name(20), activity.user.url %> + <%= _("has published on community %s") % link_to(activity.target.profile.short_name(20), activity.target.profile.url, :style => "color: #333; font-weight: bold; text-decoration: none;") if activity.target.profile.is_a?(Community) %> + <%= time_ago_as_sentence(activity.created_at) %> +

+

+ <%= link_to(activity.params['name'], activity.params['url'], :style => "color: #333; font-weight: bold; text-decoration: none;") %> +
+ + <%= image_tag(activity.params['first_image']) unless activity.params['first_image'].blank? %><%= strip_tags(truncate(activity.params['lead'], :length => 1000, :ommision => '...')).gsub(/(\xC2\xA0|\s)+/, ' ').gsub(/^\s+/, '') %> +

+

<%= content_tag(:p, link_to(_('See complete forum'), activity.get_url), :class => 'see-forum') if activity.target.is_a?(Forum) %>

+
+ <%= render :partial => 'profile_comments', :locals => { :activity => activity } %> +
diff --git a/app/views/person_notifier/mailer/_default_activity.rhtml b/app/views/person_notifier/mailer/_default_activity.rhtml new file mode 100644 index 0000000..5e28fe4 --- /dev/null +++ b/app/views/person_notifier/mailer/_default_activity.rhtml @@ -0,0 +1,19 @@ + + + + + + + + + +
+ <%= link_to(profile_image(activity.user, :minor), activity.user.url) %> + +

+ <%= link_to activity.user.name, activity.user.url %> <%= describe activity %> + <%= time_ago_as_sentence(activity.created_at) %> +

+
+ <%= render :partial => 'profile_comments', :locals => { :activity => activity } %> +
diff --git a/app/views/person_notifier/mailer/_join_community.rhtml b/app/views/person_notifier/mailer/_join_community.rhtml new file mode 100644 index 0000000..ce1d787 --- /dev/null +++ b/app/views/person_notifier/mailer/_join_community.rhtml @@ -0,0 +1 @@ +<%= render :partial => 'default_activity', :locals => { :activity => activity } %> diff --git a/app/views/person_notifier/mailer/_leave_scrap.rhtml b/app/views/person_notifier/mailer/_leave_scrap.rhtml new file mode 100644 index 0000000..ce1d787 --- /dev/null +++ b/app/views/person_notifier/mailer/_leave_scrap.rhtml @@ -0,0 +1 @@ +<%= render :partial => 'default_activity', :locals => { :activity => activity } %> diff --git a/app/views/person_notifier/mailer/_leave_scrap_to_self.rhtml b/app/views/person_notifier/mailer/_leave_scrap_to_self.rhtml new file mode 120000 index 0000000..56c0e42 --- /dev/null +++ b/app/views/person_notifier/mailer/_leave_scrap_to_self.rhtml @@ -0,0 +1 @@ +_leave_scrap.rhtml \ No newline at end of file diff --git a/app/views/person_notifier/mailer/_new_friendship.rhtml b/app/views/person_notifier/mailer/_new_friendship.rhtml new file mode 100644 index 0000000..ce1d787 --- /dev/null +++ b/app/views/person_notifier/mailer/_new_friendship.rhtml @@ -0,0 +1 @@ +<%= render :partial => 'default_activity', :locals => { :activity => activity } %> diff --git a/app/views/person_notifier/mailer/_profile_comments.rhtml b/app/views/person_notifier/mailer/_profile_comments.rhtml new file mode 100644 index 0000000..0f1333e --- /dev/null +++ b/app/views/person_notifier/mailer/_profile_comments.rhtml @@ -0,0 +1,13 @@ +<% if activity.comments_count > 2 %> +
+ <% if activity.params['url'].blank? %> + <%= _("%s comments") % activity.comments_count %> + <% else %> + <%= link_to(_("View all %s comments") % activity.comments_count, activity.params['url']) %> + <% end %> +
+<% else %> + +<% end %> diff --git a/app/views/person_notifier/mailer/_reply_scrap_on_self.rhtml b/app/views/person_notifier/mailer/_reply_scrap_on_self.rhtml new file mode 120000 index 0000000..56c0e42 --- /dev/null +++ b/app/views/person_notifier/mailer/_reply_scrap_on_self.rhtml @@ -0,0 +1 @@ +_leave_scrap.rhtml \ No newline at end of file diff --git a/app/views/person_notifier/mailer/_upload_image.rhtml b/app/views/person_notifier/mailer/_upload_image.rhtml new file mode 100644 index 0000000..058a163 --- /dev/null +++ b/app/views/person_notifier/mailer/_upload_image.rhtml @@ -0,0 +1,15 @@ + + + + + +
+ <%= link_to(profile_image(activity.user, :minor), activity.user.url) %> + +

+ <%= link_to activity.user.name, activity.user.url %> <%= describe activity %> + <%= time_ago_as_sentence(activity.created_at) %> +

+
+
+
diff --git a/app/views/person_notifier/mailer/content_summary.rhtml b/app/views/person_notifier/mailer/content_summary.rhtml new file mode 100644 index 0000000..d56c2f6 --- /dev/null +++ b/app/views/person_notifier/mailer/content_summary.rhtml @@ -0,0 +1,18 @@ +

<%= _("%s's network activity") % @profile.name %>

+
+
+<% @notifications.each do |activity| %> +
+ <%= render :partial => activity.verb, :locals => { :activity => activity } rescue "cannot render notification for #{activity.verb}" %> +
+<% end %> +
+ +
+

<%= _("Greetings,") %>

+
+

--

+

<%= _('%s team.') % @environment %>

+

<%= url_for @url %>

+
+
diff --git a/app/views/profile_editor/_person.rhtml b/app/views/profile_editor/_person.rhtml index fc50c35..5ec85d3 100644 --- a/app/views/profile_editor/_person.rhtml +++ b/app/views/profile_editor/_person.rhtml @@ -19,3 +19,9 @@ <%= @plugins.dispatch(:profile_info_extra_contents).collect { |content| instance_eval(&content) }.join("") %> <%= render :partial => 'person_form', :locals => {:f => f} %> + +

<%= _('Notification options') %>

+
+ <%= select_tag 'profile_data[notification_time]', options_for_select([[_('Disabled'), 0], [_('Hourly'), 1], [_('Half Day'), 12], [_('Daily'), 24]], @profile.notification_time) %> +
+ diff --git a/config/initializers/delayed_job_config.rb b/config/initializers/delayed_job_config.rb index 9edc6a8..9633ab3 100644 --- a/config/initializers/delayed_job_config.rb +++ b/config/initializers/delayed_job_config.rb @@ -1,3 +1,4 @@ Delayed::Worker.backend = :active_record Delayed::Worker.max_attempts = 2 Delayed::Worker.max_run_time = 10.minutes +Delayed::Worker.destroy_failed_jobs = false diff --git a/config/initializers/person_notification.rb b/config/initializers/person_notification.rb new file mode 100644 index 0000000..0c2fd47 --- /dev/null +++ b/config/initializers/person_notification.rb @@ -0,0 +1 @@ +PersonNotifier.schedule_all_next_notification_mail diff --git a/lib/acts_as_having_settings.rb b/lib/acts_as_having_settings.rb index 1d17728..e808b11 100644 --- a/lib/acts_as_having_settings.rb +++ b/lib/acts_as_having_settings.rb @@ -14,6 +14,17 @@ module ActsAsHavingSettings def #{settings_field} self[:#{settings_field}] ||= Hash.new end + + def setting_changed?(setting_field) + setting_field = setting_field.to_sym + changed_settings = self.changes['#{settings_field}'] + return false if changed_settings.nil? + + old_setting_value = changed_settings.first.nil? ? nil : changed_settings.first[setting_field] + new_setting_value = changed_settings.last[setting_field] + old_setting_value != new_setting_value + end + before_save :symbolize_settings_keys private def symbolize_settings_keys @@ -36,11 +47,9 @@ module ActsAsHavingSettings val.nil? ? (#{default}.is_a?(String) ? gettext(#{default}) : #{default}) : val end def #{setting}=(value) - - #UPGRADE Leandro: I add this line to save the serialize attribute - send(self.class.settings_field.to_s + '_will_change!') - - send(self.class.settings_field)[:#{setting}] = self.class.acts_as_having_settings_type_cast(value, #{data_type.inspect}) + h = send(self.class.settings_field).clone + h[:#{setting}] = self.class.acts_as_having_settings_type_cast(value, #{data_type.inspect}) + send(self.class.settings_field.to_s + '=', h) end CODE end diff --git a/test/unit/acts_as_having_settings_test.rb b/test/unit/acts_as_having_settings_test.rb index 0c09b72..57d737c 100644 --- a/test/unit/acts_as_having_settings_test.rb +++ b/test/unit/acts_as_having_settings_test.rb @@ -80,4 +80,38 @@ class ActsAsHavingSettingsTest < ActiveSupport::TestCase assert obj.save end + should 'setting_changed be true if a setting passed as parameter was changed' do + obj = TestClass.new + obj.flag= true + assert obj.setting_changed? 'flag' + end + + should 'setting_changed be false if a setting passed as parameter was not changed' do + obj = TestClass.new + assert !obj.setting_changed?('flag') + end + + should 'setting_changed be false if a setting passed as parameter was changed with the same value' do + obj = TestClass.new + obj.flag= true + obj.save + obj.flag= true + assert !obj.setting_changed?('flag') + end + + should 'setting_changed be false if a setting passed as parameter was not changed but another setting is changed' do + obj = TestClass.new(:name => 'some name') + obj.save + obj.name= 'antoher nme' + assert !obj.setting_changed?('flag') + end + + should 'setting_changed be true for all changed fields' do + obj = TestClass.new(:name => 'some name', :flag => false) + obj.save + obj.name= 'another nme' + obj.flag= true + assert obj.setting_changed?('flag') + assert obj.setting_changed?('name') + end end diff --git a/test/unit/person_notifier_helper_test.rb b/test/unit/person_notifier_helper_test.rb new file mode 100644 index 0000000..a574c4f --- /dev/null +++ b/test/unit/person_notifier_helper_test.rb @@ -0,0 +1,24 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class PersonNotifierHelperTest < ActiveSupport::TestCase + + include PersonNotifierHelper + include ActionView::Helpers::TagHelper + + def setup + @profile = mock + @env = Environment.new + end + attr_reader :profile, :env + + should 'append top url of environment at image path' do + profile.expects(:environment).returns(env).at_least_once + assert_match /src="http:\/\/localhost\/image.png"/, image_tag("/image.png") + end + + should 'return original path if do not have an environment' do + profile.expects(:environment).returns(nil).at_least_once + assert_match /src="\/image.png"/, image_tag("/image.png") + end + +end diff --git a/test/unit/person_notifier_test.rb b/test/unit/person_notifier_test.rb new file mode 100644 index 0000000..491ac07 --- /dev/null +++ b/test/unit/person_notifier_test.rb @@ -0,0 +1,213 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class PersonNotifierTest < ActiveSupport::TestCase + FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures' + CHARSET = "utf-8" + + def setup + ActionMailer::Base.delivery_method = :test + ActionMailer::Base.perform_deliveries = true + ActionMailer::Base.deliveries = [] + Person.destroy_all + @admin = create_user('adminuser').person + @member = create_user('member').person + @admin.notification_time = 24 + @member.notification_time = 24 + @admin.save! + @member.save! + @community = fast_create(Community) + @community.add_member(@admin) + @article = fast_create(TextileArticle, :name => 'Article test', :profile_id => @community.id, :notify_comments => true) + Delayed::Job.destroy_all + end + + should 'deliver mail to community members' do + @community.add_member(@member) + notify + sent = ActionMailer::Base.deliveries.first + assert_equal [@member.email], sent.to + end + + should 'do not send mail if do not have notifications' do + @community.add_member(@member) + ActionTracker::Record.delete_all + notify + assert ActionMailer::Base.deliveries.empty? + end + + should 'do not send mail to people not joined to community' do + Comment.create!(:author => @admin, :title => 'test comment 2', :body => 'body 2!', :source => @article) + notify + sent = ActionMailer::Base.deliveries.first + assert !sent + end + + should 'display author name in delivered mail' do + @community.add_member(@member) + Comment.create!(:author => @admin, :title => 'test comment', :body => 'body!', :source => @article) + notify + sent = ActionMailer::Base.deliveries.first + assert_match /#{@admin.name}/, sent.body + end + + should 'do not include comment created before last notification' do + @community.add_member(@member) + ActionTracker::Record.delete_all + comment = Comment.create!(:author => @admin, :title => 'test comment', :body => 'body!', :source => @article ) + @member.last_notification = DateTime.now + 1.day + notify + assert ActionMailer::Base.deliveries.empty? + end + + should 'update last notification date' do + Comment.create!(:author => @admin, :title => 'test comment 2', :body => 'body 2!', :source => @article) + @community.add_member(@member) + assert_equal nil, @member.last_notification + notify + assert @member.last_notification + end + + should 'reschedule after notification' do + Comment.create!(:author => @admin, :title => 'test comment 2', :body => 'body 2!', :source => @article) + @community.add_member(@member) + assert_equal nil, @member.last_notification + notify + assert PersonNotifier::NotifyJob.find(@member.id) + end + + should 'schedule next mail at notification time' do + @member.notification_time = 12 + @member.notifier.schedule_next_notification_mail + assert_equal @member.notification_time, ((Delayed::Job.first.run_at - DateTime.now)/1.hour).round + end + + should 'do not schedule duplicated notification mail' do + @member.notification_time = 12 + @member.notifier.schedule_next_notification_mail + @member.notifier.schedule_next_notification_mail + assert_equal 1, Delayed::Job.count + end + + should 'do not schedule next mail if notification time is zero' do + @member.notification_time = 0 + @member.notifier.schedule_next_notification_mail + assert_equal 0, Delayed::Job.count + end + + should 'schedule next notifications for all person with notification time greater than zero' do + @member.notification_time = 1 + @admin.notification_time = 0 + @admin.save! + @member.save! + Delayed::Job.delete_all + PersonNotifier.schedule_all_next_notification_mail + process_delayed_job_queue + assert_equal 1, Delayed::Job.count + end + + should 'do not create duplicated job' do + PersonNotifier.schedule_all_next_notification_mail + PersonNotifier.schedule_all_next_notification_mail + assert_equal 1, Delayed::Job.count + end + + should 'schedule after update and set a valid notification time' do + @member.notification_time = 0 + @member.save! + assert_equal 0, Delayed::Job.count + @member.notification_time = 12 + @member.save! + assert_equal 1, Delayed::Job.count + end + + should 'reschedule with changed notification time' do + @member.notification_time = 2 + @member.save! + assert_equal 1, Delayed::Job.count + @member.notification_time = 12 + @member.save! + assert_equal 1, Delayed::Job.count + assert_equal @member.notification_time, ((Delayed::Job.first.run_at - DateTime.now)/1.hour).round + end + + should 'display error message if fail to render a notificiation' do + @community.add_member(@member) + Comment.create!(:author => @admin, :title => 'test comment', :body => 'body!', :source => @article) + ActionTracker::Record.any_instance.stubs(:verb).returns("some_invalid_verb") + notify + sent = ActionMailer::Base.deliveries.first + assert_match /cannot render notification for some_invalid_verb/, sent.body + end + + ActionTrackerConfig.verb_names.each do |verb| + should "render notification for verb #{verb}" do + action = mock() + action.stubs(:verb).returns(verb) + action.stubs(:user).returns(@member) + action.stubs(:created_at).returns(DateTime.now) + action.stubs(:target).returns(fast_create(Forum)) + action.stubs(:comments_count).returns(0) + action.stubs(:comments_as_thread).returns([]) + action.stubs(:params).returns({'name' => 'home', 'url' => '/', 'lead' => ''}) + action.stubs(:get_url).returns('') + + notifications = [] + notifications.stubs(:find).returns([action]) + Person.any_instance.stubs(:tracked_notifications).returns(notifications) + + notify + sent = ActionMailer::Base.deliveries.first + assert_no_match /cannot render notification for #{verb}/, sent.body + end + end + + should 'exists? method in NotifyAllJob return false if there is no instance of this class created' do + Delayed::Job.enqueue(PersonNotifier::NotifyJob.new) + assert !PersonNotifier::NotifyAllJob.exists? + end + + should 'exists? method in NotifyAllJob return false if there is no jobs created' do + assert !PersonNotifier::NotifyAllJob.exists? + end + + should 'exists? method in NotifyAllJob return true if there is at least one instance of this class' do + Delayed::Job.enqueue(PersonNotifier::NotifyAllJob.new) + assert PersonNotifier::NotifyAllJob.exists? + end + + should 'perform create NotifyJob for all users with notification_time' do + Delayed::Job.enqueue(PersonNotifier::NotifyAllJob.new) + process_delayed_job_queue + assert_equal 2, Delayed::Job.count + end + + should 'perform create NotifyJob for all users with notification_time defined greater than zero' do + @member.notification_time = 1 + @admin.notification_time = 0 + @admin.save! + @member.save! + Delayed::Job.delete_all + Delayed::Job.enqueue(PersonNotifier::NotifyAllJob.new) + process_delayed_job_queue + assert_equal 1, Delayed::Job.count + end + + should 'NotifyJob failed jobs create a new NotifyJob on permanent failure' do + Delayed::Job.enqueue(PersonNotifier::NotifyJob.new(@member.id)) + + PersonNotifier.any_instance.stubs(:notify).raises('error') + + process_delayed_job_queue + jobs = Delayed::Job.all + + assert 1, jobs.select{|j| j.failed?}.size + assert 1, jobs.select{|j| !j.failed?}.size + end + + def notify + ActionTracker::Record.all.map{|action| Person.notify_activity(action)} + process_delayed_job_queue + @member.notifier.notify + end + +end diff --git a/test/unit/person_test.rb b/test/unit/person_test.rb index 49f2826..52cb24a 100644 --- a/test/unit/person_test.rb +++ b/test/unit/person_test.rb @@ -1413,4 +1413,10 @@ class PersonTest < ActiveSupport::TestCase person.reload assert_equal person.activities, [] end + + should 'person notifier be PersonNotifier class' do + p = Person.new + assert p.notifier.kind_of?(PersonNotifier) + end + end -- libgit2 0.21.2