adva_activity 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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