adva_activity 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 965ecb6fbf299a3e716179dbe3ac1b72dbcc24d4
4
+ data.tar.gz: 3f6dbbb3828849f4895c76e4913a3302dd2e4a3a
5
+ SHA512:
6
+ metadata.gz: fc05fa3e5a2c9b94f65903a66b90517161037fc4a086c57bdce6e453fdff5455babcbc24ba34a52b5202adb6ae49ce277b6479ccd7296124c09bfea36b56a357
7
+ data.tar.gz: 10e9ad8ae619b7af5eee9101452278acbf8682f1251e4cca442445edec5ee1c965198bea60ae917b01bc1f16003415f343e98b0f2ca20cb61af47bee67976579
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in adva_activity.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Micah Geisel
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,29 @@
1
+ # AdvaActivity
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'adva_activity'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install adva_activity
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/adva_activity/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Micah Geisel"]
6
+ gem.email = ["micah@botandrose.com"]
7
+ gem.description = %q{Adva Activity}
8
+ gem.summary = %q{Engine for Adva CMS activity feed}
9
+ gem.homepage = ""
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "adva_activity"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = AdvaActivity::VERSION
17
+
18
+ gem.add_dependency "rails-observers"
19
+ end
@@ -0,0 +1,81 @@
1
+ .activities h3 {
2
+ font-size: 12pt;
3
+ }
4
+ .activities h4,
5
+ .activities p,
6
+ .activities blockquote {
7
+ margin: 0px 0px 3px 0px;
8
+ font-weight: normal;
9
+ }
10
+ .activities ul {
11
+ margin-top: 10px;
12
+ list-style-type: none;
13
+ }
14
+ .activities blockquote {
15
+ font-style: italic;
16
+ }
17
+ .activities .meta {
18
+ clear: right;
19
+ float: right;
20
+ margin: 0;
21
+ padding-bottom: 0;
22
+ text-align: right;
23
+ }
24
+ .activities .meta,
25
+ .activities .meta a {
26
+ font-size: 12px;
27
+ color: #888;
28
+ line-height: 20px;
29
+ }
30
+ .activities li.empty {
31
+ margin-top: 10px;
32
+ }
33
+
34
+ .activities .activity {
35
+ padding: 10px 10px 10px 35px;
36
+ }
37
+ .activities .activity.highlight {
38
+ background-color: #f6f6f6;
39
+ }
40
+
41
+ .activity.comment_created {
42
+ background: url(/assets/adva_cms/icons/comment_add.png) no-repeat 10px 11px;
43
+ }
44
+ .activity.comment_edited {
45
+ background: url(/assets/adva_cms/icons/comment_edit.png) no-repeat 10px 11px;
46
+ }
47
+ .activity.comment_deleted {
48
+ background: url(/assets/adva_cms/icons/comment_delete.png) no-repeat 10px 11px;
49
+ }
50
+ .activity.comment_approved {
51
+ background: url(/assets/adva_cms/icons/comment.png) no-repeat 10px 11px;
52
+ }
53
+ .activity.comment_unapproved {
54
+ background: url(/assets/adva_cms/icons/comment.png) no-repeat 10px 11px;
55
+ }
56
+
57
+ .activity.article_created {
58
+ background: url(/assets/adva_cms/icons/page_add.png) no-repeat 10px 11px;
59
+ }
60
+ .activity.article_revised {
61
+ background: url(/assets/adva_cms/icons/page_edit.png) no-repeat 10px 11px;
62
+ }
63
+ .activity.article_published {
64
+ background: url(/assets/adva_cms/icons/page_go.png) no-repeat 10px 11px;
65
+ }
66
+ .activity.article_unpublished {
67
+ background: url(/assets/adva_cms/icons/page_red.png) no-repeat 10px 11px;
68
+ }
69
+ .activity.article_deleted {
70
+ background: url(/assets/adva_cms/icons/page_delete.png) no-repeat 10px 11px;
71
+ }
72
+
73
+ .activity.wikipage_created {
74
+ background: url(/assets/adva_cms/icons/page_white_add.png) no-repeat 10px 11px;
75
+ }
76
+ .activity.wikipage_revised {
77
+ background: url(/assets/adva_cms/icons/page_white_edit.png) no-repeat 10px 11px;
78
+ }
79
+ .activity.wikipage_deleted {
80
+ background: url(/assets/adva_cms/icons/page_white_delete.png) no-repeat 10px 11px;
81
+ }
@@ -0,0 +1,73 @@
1
+ module ActivitiesHelper
2
+ def render_activities(activities, recent = false)
3
+ if activities.present?
4
+ html = activities.collect do |activity|
5
+ render :partial => "admin/activities/#{activity.object_type.downcase}",
6
+ :locals => { :activity => activity, :recent => recent }
7
+ end.join
8
+ else
9
+ html = %(<li class="empty shade">#{I18n.t(:'adva.activity.none')}.</li>)
10
+ end
11
+ raw %(<ul class="activities">#{html}</ul>)
12
+ end
13
+
14
+ def activity_css_classes(activity)
15
+ type = activity.object_attributes['type'] || activity.object_type
16
+ "#{type}_#{activity.all_actions.last}".downcase
17
+ # activity.all_actions.collect {|action| "#{type}-#{action}".downcase }.uniq * ' '
18
+ end
19
+
20
+ def activity_datetime(activity, short = false)
21
+ if activity.from and short
22
+ from = activity.from.to_s(:time_only)
23
+ to = activity.to.to_s(:time_only)
24
+ "#{from} - #{to}"
25
+ elsif activity.from and activity.from.to_date != activity.to.to_date
26
+ from = activity.from.to_ordinalized_s(:plain)
27
+ to = activity.to.to_ordinalized_s(:plain)
28
+ "#{from} - #{to}"
29
+ elsif activity.from
30
+ from = activity.from.to_ordinalized_s(:plain)
31
+ to = activity.to.to_ordinalized_s(:time_only)
32
+ "#{from} - #{to}"
33
+ else
34
+ activity.created_at.send *(short ? [:to_s, :time_only] : [:to_ordinalized_s, :plain])
35
+ end
36
+ end
37
+
38
+ # def activity_datetime(activity, short = false)
39
+ # from, to = if activity.from && short
40
+ # [l(activity.from, :format => :time), l(activity.to, :format => :time)]
41
+ # elsif activity.from && activity.from.to_date != activity.to.to_date
42
+ # [l(activity.from, :format => :short), l(activity.to, :format => :short)]
43
+ # elsif activity.from
44
+ # [l(activity.from, :format => :short), l(activity.to, :format => :time)]
45
+ # end
46
+
47
+ # t(:'adva.activity.from_to', :from => from, :to => to)
48
+ # end
49
+
50
+ def activity_object_edit_url(activity)
51
+ type = activity.object_attributes['type'] || activity.object_type
52
+ send "edit_admin_#{type}_path".downcase, activity.site_id, activity.section_id, activity.object_id
53
+ end
54
+
55
+ # FIXME not used anywhere?
56
+ # def activity_commentable_edit_url(activity)
57
+ # type = activity.object_attributes['commentable_type']
58
+ # send "edit_admin_#{type}_path".downcase, activity.site_id, activity.section_id, activity.commentable_id
59
+ # end
60
+
61
+ # FIXME not used anywhere?
62
+ # def link_to_activity_commentable(activity)
63
+ # link_to truncate(activity.commentable_title, 100), activity_commentable_url(activity)
64
+ # end
65
+
66
+ def link_to_activity_user(activity)
67
+ if activity.author.registered?
68
+ link_to activity.author_name, admin_site_user_path(activity.site, activity.author)
69
+ else
70
+ activity.author_link(:include_email => true)
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,87 @@
1
+ class Activity < ActiveRecord::Base
2
+ belongs_to :site
3
+ belongs_to :section
4
+ belongs_to :object, :polymorphic => true
5
+
6
+ def method_missing_with_object_attributes(name, *args)
7
+ attrs = self[:object_attributes]
8
+ return attrs[name.to_s] if attrs && attrs.has_key?(name.to_s)
9
+ method_missing_without_object_attributes name, *args
10
+ end
11
+ alias_method_chain :method_missing, :object_attributes
12
+
13
+ belongs_to_author
14
+
15
+ serialize :actions
16
+ serialize :object_attributes
17
+
18
+ validates_presence_of :site, :section, :object
19
+
20
+ attr_accessor :siblings
21
+
22
+ class << self
23
+ def find_coinciding_grouped_by_dates(*dates)
24
+ groups = (1..dates.size).collect{[]}
25
+ activities = find_coinciding #, :include => :user
26
+
27
+ # collect activities for the given dates
28
+ activities.each do |activity|
29
+ activity_date = activity.created_at.to_date
30
+ dates.each_with_index {|date, i| groups[i] << activity and break if activity_date == date }
31
+ end
32
+
33
+ # remove all found activities from the original resultset
34
+ groups.each{|group| group.each{ |activity| activities.delete(activity) }}
35
+
36
+ # push remaining resultset as a group itself (i.e. 'the rest of them')
37
+ groups << activities
38
+ end
39
+
40
+ def find_coinciding(options = {})
41
+ activities = order(created_at: :desc).limit(50)
42
+ activities = activities.group_by{|r| "#{r.object_type}#{r.object_id}"}.values
43
+ activities = group_coinciding(activities)
44
+ activities.sort{|a, b| b.created_at <=> a.created_at }
45
+ end
46
+
47
+ def group_coinciding(activities, delta = nil)
48
+ activities.inject [] do |chunks, group|
49
+ chunks << group.shift
50
+ group.each do |activity|
51
+ last = chunks.last.siblings.last || chunks.last
52
+ if last.coincides_with?(activity, delta)
53
+ chunks.last.siblings << activity
54
+ else
55
+ chunks << activity
56
+ end
57
+ end
58
+ chunks
59
+ end
60
+ end
61
+ end
62
+
63
+ after_initialize do
64
+ @siblings = []
65
+ end
66
+
67
+ def coincides_with?(other, delta = nil)
68
+ delta ||= 1.hour
69
+ created_at - other.created_at <= delta.to_i
70
+ end
71
+
72
+ # FIXME should be translated!
73
+ def all_actions
74
+ actions = Array(siblings.reverse.map(&:actions).compact.flatten) + self.actions
75
+ previous = nil
76
+ actions.reject! { |action| (action == previous).tap { previous = action } }
77
+ actions
78
+ end
79
+
80
+ def from
81
+ siblings.last.created_at if siblings.present?
82
+ end
83
+
84
+ def to
85
+ created_at
86
+ end
87
+ end
@@ -0,0 +1,11 @@
1
+ class ActivityNotifier < ActionMailer::Base
2
+ helper :content
3
+
4
+ def new_content_notification(activity, user)
5
+ recipients user.email
6
+ subject "[#{activity.site.name} / #{activity.section.title}] " +
7
+ I18n.t( :'adva.activity.notifier.new', :activity => activity.object.class )
8
+ from "#{activity.site.name} <#{activity.site.email}>"
9
+ body :activity => activity
10
+ end
11
+ end
@@ -0,0 +1,25 @@
1
+ module Activities
2
+ class ActivityObserver < ActiveRecord::Observer
3
+ observe :activity
4
+
5
+ def after_create(activity)
6
+ self.class.send(:notify_subscribers, activity)
7
+ end
8
+
9
+ private
10
+ class << self
11
+ def notify_subscribers(activity)
12
+ find_subscribers(activity).each do |subscriber|
13
+ ActivityNotifier.deliver_new_content_notification(activity, subscriber) if activity.site.email_notification
14
+ end
15
+ end
16
+
17
+ def find_subscribers(activity)
18
+ [].tap do |subscribers|
19
+ subscribers << User.by_role_and_context(:admin, activity.site)
20
+ subscribers << User.by_role_and_context(:superuser, activity.site)
21
+ end.flatten
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,20 @@
1
+ module Activities
2
+ class ArticleObserver < Activities::Logger
3
+ observe :article
4
+
5
+ logs_activity :attributes => [ :title, :type ] do |log|
6
+ log.revised :if => [:save_version?, {:not => :new_record?}]
7
+ log.published :if => [:published_at_changed?, :published?]
8
+ log.unpublished :if => [:published_at_changed?, {:not => :published?}]
9
+ end
10
+
11
+ def initialize_activity(record)
12
+ super.tap do |activity|
13
+ activity.site = record.site
14
+ activity.section = record.section
15
+ activity.author = record.author
16
+ end
17
+ end
18
+ end
19
+ end
20
+
@@ -0,0 +1,29 @@
1
+ if defined?(Comment)
2
+ module Activities
3
+ class CommentObserver < Activities::Logger
4
+
5
+ observe :comment
6
+
7
+ logs_activity do |log|
8
+ log.edited :if => [:body_changed?, {:not => :new_record?}]
9
+ log.approved :if => [:approved_changed?, :approved?]
10
+ log.unapproved :if => [:approved_changed?, :unapproved?]
11
+ end
12
+
13
+ def collect_activity_attributes(record)
14
+ attrs = record.send(:clone_attributes)
15
+ attrs = attrs.slice 'commentable_id', 'body', 'author_name', 'author_email', 'author_url'
16
+ type = record.commentable.has_attribute?('type') ? record.commentable['type'] : record.commentable_type
17
+ attrs.update('commentable_type' => type, 'commentable_title' => record.commentable.title)
18
+ end
19
+
20
+ def initialize_activity(record)
21
+ super.tap do |activity|
22
+ activity.site = record.commentable.site
23
+ activity.section = record.commentable.section
24
+ activity.author = record.author
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,94 @@
1
+ module Activities
2
+ class Logger < ActiveRecord::Observer
3
+ class_attribute :activity_attributes, :activity_conditions
4
+ self.activity_attributes = []
5
+ self.activity_conditions = [[:created, :new_record?], [:deleted, :frozen?]]
6
+
7
+ class << self
8
+ def logs_activity(options = {})
9
+ self.activity_attributes += options[:attributes] if options[:attributes]
10
+ yield Configurator.new(self) if block_given?
11
+ end
12
+ end
13
+
14
+ class Configurator
15
+ def initialize(klass)
16
+ @klass = klass
17
+ end
18
+
19
+ def method_missing(name, options)
20
+ options.assert_valid_keys :if
21
+ @klass.activity_conditions << [name, options[:if]]
22
+ end
23
+ end
24
+
25
+ def before_save(record)
26
+ prepare_activity_logging(record)
27
+ end
28
+
29
+ def after_save(record)
30
+ log_activity(record)
31
+ end
32
+
33
+ def after_destroy(record)
34
+ prepare_activity_logging(record)
35
+ log_activity(record)
36
+ end
37
+
38
+ protected
39
+
40
+ def prepare_activity_logging(record)
41
+ record.instance_variable_set :@activity, initialize_activity(record)
42
+ end
43
+
44
+ def log_activity(record)
45
+ activity = record.instance_variable_get :@activity
46
+ if activity && !activity.actions.empty?
47
+ activity.object = record
48
+ activity.author = record.author if record.respond_to? :author
49
+ activity.save!
50
+ end
51
+ record.instance_variable_set :@activity, nil
52
+ end
53
+
54
+ def initialize_activity(record)
55
+ Activity.new :actions => collect_actions(record),
56
+ :object_attributes => collect_activity_attributes(record)
57
+ end
58
+
59
+ def collect_actions(record)
60
+ activity_conditions.collect do |name, conditions|
61
+ name.to_s if conditions_satisfied?(record, conditions)
62
+ end.compact
63
+ end
64
+
65
+ def conditions_satisfied?(record, conditions)
66
+ conditions = conditions.is_a?(Array) ? conditions : [conditions]
67
+ conditions.collect do |condition|
68
+ condition_satisfied?(record, condition)
69
+ end.inject(true){|a, b| a && b }
70
+ end
71
+
72
+ def condition_satisfied?(record, condition)
73
+ case condition
74
+ when Symbol then !!record.send(condition)
75
+ when Hash then
76
+ condition.collect do |key, condition|
77
+ case key
78
+ when :not then !record.send(condition)
79
+ else raise 'not implemented' # TODO: should this be NotImplementedError?
80
+ end
81
+ end.inject(false){|a, b| a || b }
82
+ end
83
+ end
84
+
85
+ def collect_activity_attributes(record)
86
+ Hash[*activity_attributes.map do |attribute|
87
+ [attribute.to_s, case attribute
88
+ when Symbol then record.send attribute
89
+ when Proc then record.attribute.call(self)
90
+ end]
91
+ end.flatten]
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,9 @@
1
+ module Activities
2
+ class SectionObserver < ActiveRecord::Observer
3
+ observe :section
4
+
5
+ def after_destroy(record)
6
+ Activity.destroy_all(:section_id => record.id)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ <%=t :'adva.activity.view.new', :name => @activity.author_name,
2
+ :email => @activity.author_email,
3
+ :activity => @activity.object.class,
4
+ :site => @activity.site.name,
5
+ :section => @activity.section.title %>
@@ -0,0 +1,10 @@
1
+ <div class="activities">
2
+ <h3><%= t(:'adva.activity.titles.today', :date => l(Date.current, :format => :short)) %></h3>
3
+ <%= render_activities activities[0], true %>
4
+
5
+ <h3><%= t(:'adva.activity.titles.yesterday', :date => l(Date.current.yesterday, :format => :short)) %></h3>
6
+ <%= render_activities activities[1], true %>
7
+
8
+ <h3><%= t(:'adva.activity.titles.past', :date => l(Date.current.yesterday, :format => :short)) %></h3>
9
+ <%= render_activities activities[2] %>
10
+ </div>
@@ -0,0 +1,35 @@
1
+ <li class="activity <%= activity_css_classes(activity) %> <%= cycle 'highlight', '', :name => "activities" %>">
2
+ <p class="meta">
3
+ <%= activity_datetime(activity, recent) %>
4
+ <%= link_to_activity_user(activity) %>
5
+ <%= link_to_content(activity.object.commentable) if activity.object %>
6
+ </p>
7
+
8
+ <p>
9
+ <%= t(:'adva.activity.comment', :actions => activity.all_actions.collect { |a| t(:"adva.activity.action_names.#{a.downcase}" }.to_sentence) %>
10
+ </p>
11
+
12
+ <p>
13
+ <%= truncate strip_tags(activity.body), 100 %>
14
+ </p>
15
+
16
+ <% if activity.object -%>
17
+ <ul>
18
+ <% unless activity.object.commentable_type == 'Topic' %>
19
+ <% unless activity.object.approved? -%>
20
+ <li>
21
+ <%= link_to t(:'adva.common.approve'), admin_comment_path(activity.object, "comment[approved]" => 1, :return => true), :method => :put %>
22
+ </li>
23
+ <% else -%>
24
+ <li>
25
+ <%= link_to t(:'adva.common.unapprove'), admin_comment_path(activity.object, "comment[approved]" => 0, :return => true), :method => :put %>
26
+ </li>
27
+ <% end -%>
28
+ <% end -%>
29
+ <li>
30
+ <%= link_to_edit(activity.object, :url => edit_admin_comment_path(activity.object, :return => true)) %>
31
+ <%= link_to_delete(activity.object, :url => admin_comment_path(activity.object, :return => true)) %>
32
+ </li>
33
+ </ul>
34
+ <% end -%>
35
+ </li>
@@ -0,0 +1,15 @@
1
+ <li class="activity <%= activity_css_classes(activity) %> <%= cycle 'highlight', '', :name => "activities" %>">
2
+ <p class="meta">
3
+ <%= link_to_activity_user(activity) %><br />
4
+ </p>
5
+ <p class="meta">
6
+ <%= activity_datetime(activity, recent) %>
7
+ </p>
8
+
9
+ <p>
10
+ <%= t(:'adva.activity.actions', :type => t(:"adva.activity.types.#{activity.object_attributes['type'].tableize.singularize}"),
11
+ :actions => activity.all_actions.collect { |a| t(:"adva.activity.action_names.#{a.downcase}") }.to_sentence,
12
+ :activity => (activity.section ? link_to(activity.section.title, [:admin, activity.section.site, activity.section, :contents]) : "Deleted Section")).html_safe %>
13
+ </p>
14
+ <%= link_to_if activity.section, activity.title, [:edit, :admin, activity.section.try(:site), activity.section, activity.object] %>
15
+ </li>
@@ -0,0 +1,16 @@
1
+ <li class="activity <%= activity_css_classes(activity) %> <%= cycle 'highlight', '', :name => "activities" %>">
2
+ <p class="meta">
3
+ <%= activity_datetime(activity, recent) %>
4
+ <%= link_to_activity_user(activity) %>
5
+ <%= link_to_content(activity.object) if activity.object %>
6
+ </p>
7
+ <p><%= t(:'adva.activity.topic', :actions => activity.all_actions.collect { |a| t(:"adva.activity.action_names.#{a.downcase}" }.to_sentence) %></p>
8
+ <p><%= truncate strip_tags(activity.title), 100 %></p>
9
+
10
+ <% if activity.object -%>
11
+ <ul>
12
+ <li><%= link_to_edit(activity.object) %></li>
13
+ <li><%= link_to_delete(activity.object) %></li>
14
+ </ul>
15
+ <% end -%>
16
+ </li>
@@ -0,0 +1,22 @@
1
+ en:
2
+ adva:
3
+ activity:
4
+ from_to: "%{from} - %{to}"
5
+ types:
6
+ article: Article
7
+ action_names:
8
+ created: created
9
+ published: published
10
+ revised: revised
11
+ titles:
12
+ today: Today %{date}
13
+ yesterday: Yesterday %{date}
14
+ past: Before %{date}
15
+ none: Nothing happened
16
+ notifier:
17
+ new: New %{activity} posted
18
+ view:
19
+ new: "%{name} <%{email}> just posted a new %{activity} on %{site} in section %{section}."
20
+ comment: Comment %{actions}
21
+ topic: Topic %{actions}
22
+ actions: "%{type} %{actions} in %{activity}."
@@ -0,0 +1,23 @@
1
+ class CreateActivitiesTable < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :activities, :force => true do |t|
4
+ t.references :site
5
+ t.references :section
6
+
7
+ t.references :author, :polymorphic => true
8
+ t.string :author_name, :limit => 40
9
+ t.string :author_email, :limit => 40
10
+ t.string :author_homepage
11
+
12
+ t.string :actions
13
+ t.integer :object_id
14
+ t.string :object_type, :limit => 15
15
+ t.text :object_attributes
16
+ t.datetime :created_at, :null => false
17
+ end
18
+ end
19
+
20
+ def self.down
21
+ drop_table :activities
22
+ end
23
+ end
@@ -0,0 +1,33 @@
1
+ # require "adva_activity/version"
2
+ require "rails"
3
+ require "rails-observers"
4
+
5
+ module AdvaActivity
6
+ module SiteExtensions
7
+ def self.included base
8
+ base.has_many :activities, :dependent => :destroy
9
+ end
10
+
11
+ def grouped_activities
12
+ activities.find_coinciding_grouped_by_dates(Time.zone.now.to_date, 1.day.ago.to_date)
13
+ end
14
+ end
15
+
16
+ module HasManyActivities
17
+ def self.included base
18
+ base.has_many :activities, :as => :object
19
+ end
20
+ end
21
+
22
+ class Engine < Rails::Engine
23
+ initializer "add assets to precompilation list" do |app|
24
+ app.config.assets.precompile += %w(adva_activity/admin/activities.css)
25
+ end
26
+
27
+ config.to_prepare do
28
+ Site.send :include, SiteExtensions
29
+ Content.send :include, HasManyActivities
30
+ Comment.send :include, HasManyActivities if defined?(Comment)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,3 @@
1
+ module AdvaActivity
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,3 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../adva_cms/test/test_helper')
2
+
3
+ # Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + '/test_init/fixtures'
@@ -0,0 +1,75 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/../test_helper")
2
+
3
+ class ActivitiesNotifierTest < ActiveSupport::TestCase
4
+ def setup
5
+ super
6
+ ActionMailer::Base.delivery_method = :test
7
+ ActionMailer::Base.perform_deliveries = true
8
+ ActionMailer::Base.deliveries = []
9
+
10
+ # set up a mock mail
11
+ @mail = TMail::Mail.new
12
+ @mail.set_content_type('text', 'plain', { 'charset' => 'utf-8' })
13
+ @mail.mime_version = '1.0'
14
+
15
+ # need to stub out Time.now and set mail date in order to avoid automatic setting
16
+ # see http://github.com/rails/rails/commit/f73d34c131c1e371c76c5a146aac2c2bffbf96e5
17
+ stub(Time).now { Time.local(2009, 10, 8, 12, 0, 0) }
18
+ @mail.date = Time.now
19
+
20
+ @site = Site.first
21
+ @section = Section.first
22
+ @user = User.first
23
+ end
24
+
25
+ def teardown
26
+ super
27
+ ActionMailer::Base.deliveries.clear
28
+ end
29
+
30
+ test "sets the mail up correctly for articles" do
31
+ article = Article.first
32
+ activity = activity_for(article)
33
+
34
+ @mail.subject = "[#{@site.name} / #{@section.title}] New Article posted"
35
+ @mail.from = "#{@site.name} <#{@site.email}>"
36
+ @mail.body = "#{article.author_name} <#{article.author_email}> just posted a new Article on #{@site.name} in section #{@section.title}."
37
+ @mail.to = "#{@user.email}"
38
+
39
+ ActivityNotifier.create_new_content_notification(activity, @user).encoded.should == @mail.encoded
40
+ end
41
+
42
+ test "sets the mail up correctly for comments" do
43
+ comment = Comment.first
44
+ activity = activity_for(comment)
45
+
46
+ @mail.subject = "[#{@site.name} / #{@section.title}] New Comment posted"
47
+ @mail.from = "#{@site.name} <#{@site.email}>"
48
+ @mail.body = "#{comment.author_name} <#{comment.author_email}> just posted a new Comment on #{@site.name} in section #{@section.title}."
49
+ @mail.to = "#{@user.email}"
50
+
51
+ ActivityNotifier.create_new_content_notification(activity, @user).encoded.should == @mail.encoded
52
+ end
53
+
54
+ if Rails.plugin?(:adva_wiki)
55
+ test "sets the mail up correctly for wikipages" do
56
+ wikipage = Wikipage.first
57
+ activity = activity_for(wikipage)
58
+
59
+ @mail.subject = "[#{@site.name} / #{@section.title}] New Wikipage posted"
60
+ @mail.from = "#{@site.name} <#{@site.email}>"
61
+ @mail.body = "#{wikipage.author_name} <#{wikipage.author_email}> just posted a new Wikipage on #{@site.name} in section #{@section.title}."
62
+ @mail.to = "#{@user.email}"
63
+
64
+ ActivityNotifier.create_new_content_notification(activity, @user).encoded.should == @mail.encoded
65
+ end
66
+ end
67
+
68
+ def activity_for(object)
69
+ Activity.new(:site => @site, :section => @section).tap do |activity|
70
+ activity.object = object
71
+ activity.author_name = object.author_name
72
+ activity.author_email = object.author_email
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,65 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/../test_helper")
2
+
3
+ class ActivityTest < ActiveSupport::TestCase
4
+ def setup
5
+ super
6
+ @site = Site.first
7
+ @section = @site.sections.root
8
+ @user = User.first
9
+ end
10
+
11
+ # FIXME
12
+ # Can't get this to pass ... apparently RR has problems with expecting calls
13
+ # to dynamic methods?
14
+ #
15
+ # test "notify_subscribers sends emails to all subscribers" do
16
+ # users = User.all
17
+ # activity = Activity.new
18
+ #
19
+ # stub(Activities::ActivityObserver).find_subscribers(activity).returns(users)
20
+ # mock(ActivityNotifier).deliver_new_content_notification(anything, anything).times(users.size)
21
+ #
22
+ # Activities::ActivityObserver.instance.after_create(activity)
23
+ # end
24
+
25
+ test "receives #notify_subscribers when an Article gets created" do
26
+ mock(Activities::ActivityObserver).notify_subscribers(is_a(Activity))
27
+
28
+ Activity.with_observers('activities/activity_observer') do
29
+ Article.with_observers('activities/article_observer') do
30
+ Article.create! :title => 'An article', :body => 'body',
31
+ :author => @user, :site => @site, :section => @section
32
+ end
33
+ end
34
+ end
35
+
36
+ test "#find_subscribers returns only subscribed users" do
37
+ activity = Activity.new(:site => @site)
38
+ subscribers = @site.users.includes(:roles).where(['roles.name IN (?)', ['superuser', 'admin']])
39
+ assert_equal subscribers.count, Activities::ActivityObserver.send(:find_subscribers, activity).count
40
+ end
41
+
42
+ if Rails.plugin?(:adva_wiki)
43
+ test "receives #notify_subscribers when a Wikipage gets created" do
44
+ mock(Activities::ActivityObserver).notify_subscribers(is_a(Activity))
45
+
46
+ Activity.with_observers('activities/activity_observer') do
47
+ Wikipage.with_observers('activities/wikipage_observer') do
48
+ Wikipage.create! :title => 'A wikipage', :body => 'body', :author => @user,
49
+ :site => @site, :section => @section
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ test "receives #notify_subscribers when a Comment gets created" do
56
+ mock(Activities::ActivityObserver).notify_subscribers(is_a(Activity))
57
+
58
+ Activity.with_observers('activities/activity_observer') do
59
+ Comment.with_observers('activities/comment_observer') do
60
+ Comment.create! :body => 'body', :author => @user, :commentable => Article.first,
61
+ :site => @site, :section => @section
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,84 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/../test_helper")
2
+
3
+ class ActivityTest < ActiveSupport::TestCase
4
+ setup :bunch_of_activities
5
+
6
+ test 'associations' do
7
+ @activity.should belong_to(:site)
8
+ @activity.should belong_to(:section)
9
+ @activity.should belong_to(:object, :polymorphic => true)
10
+ @activity.should belong_to(:author)
11
+ end
12
+
13
+ test 'validations' do
14
+ @activity.should validate_presence_of(:site)
15
+ @activity.should validate_presence_of(:section)
16
+ @activity.should validate_presence_of(:object)
17
+ end
18
+
19
+ # CLASS METHODS
20
+
21
+ test '#find_coinciding_grouped_by_dates finds coinciding activities grouped
22
+ by given dates' do
23
+ stub(Activity).find.returns @activities
24
+ today, yesterday = Time.zone.now.to_date, 1.day.ago.to_date
25
+ result = Activity.find_coinciding_grouped_by_dates today, yesterday
26
+ result.should == [[@activity], @yesterdays, @older]
27
+ end
28
+
29
+ test '#find_coinciding finds activities and groups them to chunks coninciding
30
+ within a given time delta, hiding grouped activities in @activity.siblings' do
31
+ stub(Activity).find.returns [@activity] + @others
32
+ result = Activity.find_coinciding
33
+ result.should == [@activity]
34
+ result.first.siblings.should == @others
35
+ end
36
+
37
+ # INSTANCE METHODS
38
+
39
+ test "#coincides_with?(other) is true when the compared created_at values
40
+ differ by less/equal to the given delta value" do
41
+ @activity.coincides_with?(@others.first).should be_true
42
+ end
43
+
44
+ test "#coincides_with?(other) is false when the compared created_at values
45
+ differ by more than the given delta value" do
46
+ @activity.coincides_with?(@others.last).should be_false
47
+ end
48
+
49
+ test "#from returns the last sibling's created_at value" do
50
+ @activity.siblings = @others
51
+ @activity.from.should == @others.last.created_at
52
+ end
53
+
54
+ test "#to return the activity's created_at value" do
55
+ @activity.siblings = @others
56
+ @activity.to.should == @activity.created_at
57
+ end
58
+
59
+ test "#all_actions returns all actions from all siblings in a chronological order" do
60
+ @activity.siblings = @others
61
+ @activity.all_actions.should == ['created', 'edited', 'revised']
62
+ end
63
+
64
+ test "when a missing method is called it looks for a corresponding key in object_attributes" do
65
+ @activity.object_attributes = { 'foo' => 'bar' }
66
+ @activity.foo.should == 'bar'
67
+ end
68
+
69
+
70
+ def bunch_of_activities
71
+ attributes = {:object_type => 'Article', :object_id => 1}
72
+ @activity = Activity.new attributes.update(:actions => ['edited', 'revised'], :created_at => Time.zone.now)
73
+ @others = [10, 70, 80].collect do |minutes_ago|
74
+ actions = minutes_ago == 80 ? ['created'] : ['edited']
75
+ Activity.new attributes.update(:actions => actions, :created_at => minutes_ago.minutes.ago)
76
+ end
77
+
78
+ @yesterdays = [Activity.new(attributes.update(:created_at => 1.day.ago))]
79
+ @older = [Activity.new(attributes.update(:created_at => 2.days.ago))]
80
+
81
+ @activities = [@activity] + @others + @yesterdays + @older
82
+ @activities.sort!{|left, right| right.created_at <=> left.created_at }
83
+ end
84
+ end
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: adva_activity
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Micah Geisel
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-02-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails-observers
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: Adva Activity
28
+ email:
29
+ - micah@botandrose.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - Gemfile
35
+ - LICENSE
36
+ - README.md
37
+ - Rakefile
38
+ - adva_activity.gemspec
39
+ - app/assets/stylesheets/adva_activity/admin/activities.css
40
+ - app/helpers/activities_helper.rb
41
+ - app/models/activity.rb
42
+ - app/models/activity_notifier.rb
43
+ - app/observers/activities/activity_observer.rb
44
+ - app/observers/activities/article_observer.rb
45
+ - app/observers/activities/comment_observer.rb
46
+ - app/observers/activities/logger.rb
47
+ - app/observers/activities/section_observer.rb
48
+ - app/views/activity_notifier/new_content_notification.html.erb
49
+ - app/views/admin/activities/_activities.html.erb
50
+ - app/views/admin/activities/_comment.html.erb
51
+ - app/views/admin/activities/_content.html.erb
52
+ - app/views/admin/activities/_topic.html.erb
53
+ - config/locales/en.yml
54
+ - db/migrate/20080401000000_create_activities_table.rb
55
+ - lib/adva_activity.rb
56
+ - lib/adva_activity/version.rb
57
+ - test/test_helper.rb
58
+ - test/unit/activities_notifier_test.rb
59
+ - test/unit/activity_observer_test.rb
60
+ - test/unit/activity_test.rb
61
+ homepage: ''
62
+ licenses: []
63
+ metadata: {}
64
+ post_install_message:
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ requirements: []
79
+ rubyforge_project:
80
+ rubygems_version: 2.4.6
81
+ signing_key:
82
+ specification_version: 4
83
+ summary: Engine for Adva CMS activity feed
84
+ test_files:
85
+ - test/test_helper.rb
86
+ - test/unit/activities_notifier_test.rb
87
+ - test/unit/activity_observer_test.rb
88
+ - test/unit/activity_test.rb