muck-activity 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Justin Ball
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,65 @@
1
+ = MuckActivity
2
+
3
+ == Installation
4
+ The muck activity engine is part of the muck framework and relies upon the muck_engine. The main engine can be found here:
5
+ http://github.com/jbasdf/muck_engine
6
+
7
+ The easiest way to get started with muck is to generate your application using a template:
8
+ $ rails <your-app> -m http://github.com/jbasdf/rails-templates/raw/master/muck.rb
9
+
10
+ Add optional functionality with the following command:
11
+ $ rake rails:template LOCATION=http://github.com/jbasdf/rails-templates/raw/master/mucktoo.rb
12
+
13
+ == Usage
14
+ This engine implements simple activity tracking.
15
+
16
+ Models that can have activity feed should call 'has_activities'
17
+
18
+ Example:
19
+ class User
20
+ has_activities
21
+ end
22
+
23
+ Adding an item to the activity feed requires 'acts_as_activity_source' which then profiles the method 'add_activity'
24
+
25
+ add_activity(feed_to, actor, item, template, check_method)
26
+
27
+ feed_to - contains an array of objects (typically users) that have use acts_as_activity_user
28
+ actor - the user performing the action
29
+ item - a reference to the object that
30
+ template - the template (partial) to use to render the activity
31
+ check_method - and optional method to call on each object in the feed_to array that determines whether or not to add the activity
32
+
33
+ Example:
34
+ add_activity(user.feed_to, user, comment, 'comment')
35
+
36
+ === Authorization
37
+ By default 'has_activities' will add a simple 'can_view?' method:
38
+
39
+ def can_view?(check_object)
40
+ self == check_object
41
+ end
42
+
43
+ This method determines whether or not the check_object has access to the current object's activity feeds. For example, if the
44
+ intent is to recover the activities for a given user where the user is '@parent' thus:
45
+
46
+ @parent.can_view?(current_user)
47
+
48
+ The can view method will determine whether or not the current_user has the right to view the activity feed.
49
+
50
+ In most instances you will need to override this method to implement proper security. For example, for a group that has an activity feed
51
+ you might add a can_view? method like this:
52
+
53
+ def can_view?(check_object)
54
+ self.member?(check_object) || (check_object.is_a?(User) && check_object.admin?)
55
+ end
56
+
57
+ == Live Updates
58
+ If you would like to add live updates to the user's activity feed you can do so by adding an entry to GlobalConfig.yml (configuration file added by muck)
59
+
60
+ enable_live_activity_updates: true
61
+ live_activity_update_interval: 5
62
+
63
+ Note that this will poll the server every 5 seconds and so will increase server load and bandwidth usage.
64
+
65
+ Copyright (c) 2009 Justin Ball, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,76 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ desc 'Test the muck_activity plugin.'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'lib'
11
+ t.libs << 'test'
12
+ t.pattern = 'test/**/*_test.rb'
13
+ t.verbose = true
14
+ end
15
+
16
+ desc 'Generate documentation for the muck_activity plugin.'
17
+ Rake::RDocTask.new(:rdoc) do |rdoc|
18
+ rdoc.rdoc_dir = 'rdoc'
19
+ rdoc.title = 'MuckActivity'
20
+ rdoc.options << '--line-numbers' << '--inline-source'
21
+ rdoc.rdoc_files.include('README')
22
+ rdoc.rdoc_files.include('lib/**/*.rb')
23
+ end
24
+
25
+ begin
26
+ require 'jeweler'
27
+ Jeweler::Tasks.new do |gemspec|
28
+ gemspec.name = "muck-activity"
29
+ gemspec.summary = "Activity engine for the muck system"
30
+ gemspec.email = "justinball@gmail.com"
31
+ gemspec.homepage = "http://github.com/jbasdf/muck_activity"
32
+ gemspec.description = "Activity engine for the muck system."
33
+ gemspec.authors = ["Justin Ball"]
34
+ gemspec.rubyforge_project = 'muck-activity'
35
+ gemspec.add_dependency "muck-engine"
36
+ gemspec.add_dependency "muck-users"
37
+ gemspec.files.include %w( lib/muck_users
38
+ tasks/*
39
+ db/migrate/*.rb
40
+ app/**/**/**/*
41
+ config/*
42
+ locales/*
43
+ rails/*
44
+ test/*
45
+ lib/**/* )
46
+ end
47
+ rescue LoadError
48
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
49
+ end
50
+
51
+ # rubyforge tasks
52
+ begin
53
+ require 'rake/contrib/sshpublisher'
54
+ namespace :rubyforge do
55
+
56
+ desc "Release gem and RDoc documentation to RubyForge"
57
+ task :release => ["rubyforge:release:gem", "rubyforge:release:docs"]
58
+
59
+ namespace :release do
60
+ desc "Publish RDoc to RubyForge."
61
+ task :docs => [:rdoc] do
62
+ config = YAML.load(
63
+ File.read(File.expand_path('~/.rubyforge/user-config.yml'))
64
+ )
65
+
66
+ host = "#{config['username']}@rubyforge.org"
67
+ remote_dir = "/var/www/gforge-projects/muck_activity/"
68
+ local_dir = 'rdoc'
69
+
70
+ Rake::SshDirPublisher.new(host, remote_dir, local_dir).upload
71
+ end
72
+ end
73
+ end
74
+ rescue LoadError
75
+ puts "Rake SshDirPublisher is unavailable or your rubyforge environment is not configured."
76
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.1
@@ -0,0 +1,155 @@
1
+ class Muck::ActivitiesController < ApplicationController
2
+ unloadable
3
+
4
+ include ApplicationHelper
5
+
6
+ before_filter :login_required
7
+ before_filter :find_parent, :only => [:index, :create]
8
+ before_filter :get_activity, :only => [:destroy]
9
+
10
+ def index
11
+ if @parent.can_view?(current_user)
12
+ if params[:latest_activity_id]
13
+ @activities = @parent.activities.after(params[:latest_activity_id]).paginate(:page => @page, :per_page => @per_page)
14
+ else
15
+ @activities = @parent.activities.paginate(:page => @page, :per_page => @per_page)
16
+ end
17
+ respond_to do |format|
18
+ format.js do
19
+ render :partial => "activities/cached_activities", :locals => {:activities => @activities}
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ def create
26
+ @activity = @parent.activities.build(params[:activity])
27
+ @activity.source = @parent
28
+ @activity.content.gsub!(@parent.display_name, '')
29
+ @parent.save!
30
+
31
+ @activity_html = get_activity_html(@activity)
32
+ if @activity.template == 'status_update'
33
+ @status_html = get_status_html(@parent)
34
+ else
35
+ @status_html = ''
36
+ end
37
+ respond_to do |format|
38
+ format.js do
39
+ render :update do |page|
40
+ page.insert_html :top, 'activity-feed-content', @activity_html
41
+ page.replace_html 'current-status', @status_html if @status_html
42
+ page.visual_effect :highlight, "#{@activity.dom_id}".to_sym
43
+ page << 'jQuery("#status_update").val(\'\');'
44
+ page << "jQuery('#status-update-field').removeClass('status-update-lit');"
45
+ page << "jQuery('#status-update-field').addClass('status-update-dim');"
46
+ page << 'jQuery("#status-update-field").show();'
47
+ page << 'jQuery("#submit_status").show();'
48
+ page << 'jQuery("#progress-bar").hide();'
49
+ page << 'setup_submit_delete();'
50
+ end
51
+ end
52
+ format.html do
53
+ redirect_back_or_default(@parent)
54
+ end
55
+ format.json do
56
+ render :json => { :success => true,
57
+ :is_status_update => @activity.is_status_update,
58
+ :html => @activity_html,
59
+ :status_html => @status_html,
60
+ :message => t("muck.activities.created") }
61
+ end
62
+ end
63
+
64
+ rescue => ex
65
+
66
+ if @activity
67
+ errors = @activity.errors.full_messages.to_sentence
68
+ else
69
+ errors = ex
70
+ end
71
+ message = t('muck.activities.update_error', :errors => errors)
72
+ respond_to do |format|
73
+ format.html do
74
+ flash[:error] = message
75
+ redirect_back_or_default(@parent)
76
+ end
77
+ format.js do
78
+ render :update do |page|
79
+ page_alert(page, message)
80
+ end
81
+ end
82
+ end
83
+
84
+ end
85
+
86
+ def destroy
87
+ @activities_object = @activity.source
88
+ @activity.destroy
89
+ respond_to do |format|
90
+ format.html do
91
+ flash[:notice] = t("muck.activities.item_removed")
92
+ redirect_back_or_default(@activities_object)
93
+ end
94
+ format.js do
95
+ if @activity.is_status_update
96
+ @new_status = get_status_html(@activities_object)
97
+ render(:update) do |page|
98
+ page << "jQuery('#activity_#{@activity.id}').fadeOut();"
99
+ page.replace_html 'current-status', @new_status if @new_status
100
+ end
101
+ end
102
+ end
103
+ format.json do
104
+ @new_status = get_status_html(@activities_object)
105
+ render :json => { :success => true,
106
+ :is_status_update => @activity.is_status_update,
107
+ :html => @new_status,
108
+ :message => t("muck.activities.item_removed") }
109
+ end
110
+ end
111
+
112
+ rescue => ex
113
+ respond_to do |format|
114
+ format.html do
115
+ flash[:notice] = t("muck.activities.item_could_not_be_removed")
116
+ redirect_back_or_default(current_user)
117
+ end
118
+ format.js do
119
+ render(:update){ |page| page_alert(page, t("muck.activities.item_could_not_be_removed")) }
120
+ end
121
+ format.json { render :json => { :success => false, :message => t("muck.activities.item_could_not_be_removed") } }
122
+ end
123
+ end
124
+
125
+ protected
126
+
127
+ def get_status_html(activity)
128
+ @template.template_format = :html
129
+ render_to_string(:partial => 'activities/current_status', :locals => {:activities_object => activity})
130
+ end
131
+
132
+ def get_activity_html(activity)
133
+ render_to_string(:partial => "activity_templates/#{activity.template}", :locals => { :activity => activity })
134
+ end
135
+
136
+ def find_parent
137
+ @klass = params[:parent_type].to_s.capitalize.constantize
138
+ @parent = @klass.find(params[:parent_id])
139
+ end
140
+
141
+ def get_activity
142
+ @activity = Activity.find(params[:id])
143
+ unless @activity.can_edit?(current_user)
144
+ respond_to do |format|
145
+ format.html do
146
+ flash[:notice] = t("muck.activities.permission_denied")
147
+ redirect_back_or_default(current_user)
148
+ end
149
+ format.js { render(:update){|page| page_alert(page, t("muck.activities.permission_denied"))}}
150
+ end
151
+ end
152
+
153
+ end
154
+
155
+ end
@@ -0,0 +1,16 @@
1
+ module MuckActivityHelper
2
+
3
+ def activity_feed_for(activities_object)
4
+ activities = activities_object.activities.paginate(:page => @page, :per_page => @per_page)
5
+ render :partial => 'activities/activity_feed', :locals => { :activities_object => activities_object, :activities => activities }
6
+ end
7
+
8
+ def status_update(activities_object)
9
+ render :partial => 'activities/status_update', :locals => { :activities_object => activities_object }
10
+ end
11
+
12
+ def delete_activity(activity, button_type = :button, button_text = t("muck.activities.clear"))
13
+ render :partial => 'activities/delete', :locals => { :activity => activity, :button_type => button_type, :button_text => button_text }
14
+ end
15
+
16
+ end
@@ -0,0 +1,50 @@
1
+ # == Schema Information
2
+ #
3
+ # Table name: activities
4
+ #
5
+ # id :integer(4) not null, primary key
6
+ # item_id :integer(4)
7
+ # item_type :string(255)
8
+ # template :string(255)
9
+ # source_id :integer(4)
10
+ # source_type :string(255)
11
+ # content :text
12
+ # title :string(255)
13
+ # status_update :boolean(1)
14
+ # created_at :datetime
15
+ # updated_at :datetime
16
+ #
17
+
18
+ class Activity < ActiveRecord::Base
19
+
20
+ belongs_to :item, :polymorphic => true
21
+ belongs_to :source, :polymorphic => true
22
+ has_many :activity_feeds
23
+
24
+ validates_presence_of :source
25
+
26
+ named_scope :since, lambda { |time| {:conditions => ["activities.created_at > ?", time] } }
27
+ named_scope :before, lambda {|time| {:conditions => ["activities.created_at < ?", time] } }
28
+ named_scope :recent, :order => "activities.created_at DESC"
29
+ named_scope :after, lambda {|id| {:conditions => ["activities.id > ?", id] } }
30
+ named_scope :only_public, :conditions => ["activities.is_public = true"]
31
+
32
+ def validate
33
+ errors.add_to_base(I18n.t('muck.activities.template_or_item_required')) if template.blank? && item.blank?
34
+ end
35
+
36
+ # Provides a template that can be used to render a view of this activity.
37
+ # If 'template' is not specified when the object created then the item class
38
+ # name will be used to generated a template
39
+ def partial
40
+ template || item.class.name.underscore
41
+ end
42
+
43
+ # Checks to see if the specified object can edit this activity.
44
+ # Most likely check_object will be a user
45
+ def can_edit?(check_object)
46
+ return true if check_object.is_a?(User) && check_object.admin?
47
+ source == check_object
48
+ end
49
+
50
+ end
@@ -0,0 +1,14 @@
1
+ # == Schema Information
2
+ #
3
+ # Table name: activity_feeds
4
+ #
5
+ # id :integer(4) not null, primary key
6
+ # activity_id :integer(4)
7
+ # ownable_id :integer(4)
8
+ # ownable_type :string(255)
9
+ #
10
+
11
+ class ActivityFeed < ActiveRecord::Base
12
+ belongs_to :activity
13
+ belongs_to :ownable, :polymorphic => true
14
+ end
@@ -0,0 +1,26 @@
1
+ <div id="activity-feed">
2
+ <h2><%= t('muck.activities.activity_feed_title') %></h2>
3
+ <div id="activity-feed-content">
4
+ <%= render( :partial => "activities/cached_activities", :locals => {:activities => activities}) %>
5
+ </div>
6
+ </div>
7
+ <%= will_paginate activities, :previous_label => t('muck.activities.paging_newer'), :next_label => t('muck.activities.paging_older') -%>
8
+ <% content_for :javascript do -%>
9
+ jQuery(document).ready(function(){
10
+ setup_submit_delete();
11
+ <% if GlobalConfig.enable_live_activity_updates -%>
12
+ setInterval(function() {jQuery.ajax({success:function(request){update_feed(request);},url:'<%= activities_path(:parent_id => activities_object, :parent_type => activities_object.type, :format => 'js') %>&amp;latest_activity_id=' + get_latest_activity_id()})}, <%=GlobalConfig.live_activity_update_interval%> * 1000)
13
+ <% end -%>
14
+ });
15
+ function get_latest_activity_id(){
16
+ var activities = jQuery('#activity-feed-content').children('.activity-status-update')
17
+ if(activities.length > 0){
18
+ return activities[0].id.replace('activity_', '');
19
+ } else {
20
+ return '';
21
+ }
22
+ }
23
+ function update_feed(request){
24
+ jQuery('#activity-feed-content').prepend(request);
25
+ }
26
+ <% end -%>
@@ -0,0 +1,7 @@
1
+ <% if !activities.blank? -%>
2
+ <% activities.each do |activity| -%>
3
+ <% cache ("activities/#{I18n.locale}/#{activity.id}") do -%>
4
+ <%= render( :partial => "activity_templates/#{activity.partial}", :locals => {:activity => activity}) rescue '' %>
5
+ <% end -%>
6
+ <% end -%>
7
+ <% end -%>
@@ -0,0 +1,15 @@
1
+ <% if activities_object.status
2
+ status = h(activities_object.status.content)
3
+ time = activities_object.status.created_at
4
+ else
5
+ status = t('muck.activities.update_status_message')
6
+ time = false
7
+ end -%>
8
+ <span id="current-status-name"><%= activities_object.display_name %></span>
9
+ <span id="current-status-text"><%= status %></span>
10
+ <span id="status-time" class="status-time"><%= t('muck.activities.time_ago', :time_in_words => time_ago_in_words(time)) if time %></span>
11
+ <span id="status-clear">
12
+ <% if activities_object.status -%>
13
+ - <%= delete_activity(activities_object.status, :text, t("muck.activities.clear")) %>
14
+ <% end -%>
15
+ </span>
@@ -0,0 +1,11 @@
1
+ <% if button_type == :text -%>
2
+ <%= link_to_remote( button_text, :url => activity_path(activity, :format => 'js'), :method => :delete) %>
3
+ <% else -%>
4
+ <% remote_form_for(:activity, :url => activity_path(activity, :format => 'js'), :html => { :class => "delete-form activity-delete", :method => :delete } ) do |f| -%>
5
+ <% if button_type == :image -%>
6
+ <%= image_submit_tag '/images/icons/delete.png', {:title => button_text, :width => '15', :height => '15', :alt => button_text } %>
7
+ <% else -%>
8
+ <%= submit_tag button_text, { :title => button_text } %>
9
+ <% end -%>
10
+ <% end -%>
11
+ <% end -%>
@@ -0,0 +1,38 @@
1
+ <div id="status-update">
2
+ <%= output_errors(t('muck.users.problem_with_status'), {:class => 'help-box'}, @activity) %>
3
+
4
+ <div id="activity-icon"><%= icon activities_object %></div>
5
+ <div id="current-status"><%= render :partial => 'activities/current_status', :locals => {:activities_object => activities_object} %></div>
6
+ <% remote_form_for(:activity,
7
+ :url => activities_path(:parent_id => activities_object, :parent_type => activities_object.type, :format => 'js'),
8
+ :html => { :id => 'status_update_form',
9
+ :action => activities_path(:parent_id => activities_object, :parent_type => activities_object.type) } ) do |f| -%>
10
+ <div id="progress-bar" style="display:none;">
11
+ <h3><%= t('muck.activities.updating_status_message') %></h3>
12
+ <img src="/images/loading.gif" alt="progress bar">
13
+ </div>
14
+ <div id="status-update-field" class="status-update-dim">
15
+ <%= t("muck.activities.status_update_prompt") %>
16
+ <%= f.text_field :content, :id => 'status_update' %>
17
+ <span id="status-update-button">
18
+ <%= f.submit t('muck.activities.post'), :id => 'submit_status', :class=>"button" %>
19
+ </span>
20
+ </div>
21
+ <%= f.hidden_field :template, :value => 'status_update' %>
22
+ <%= f.hidden_field :is_status_update, :value => true %>
23
+ <% end %>
24
+ </div>
25
+ <% content_for :javascript do -%>
26
+ jQuery(document).ready(function() {
27
+ jQuery('#status_update').click(function(){
28
+ jQuery('#status-update-field').removeClass('status-update-dim');
29
+ jQuery('#status-update-field').addClass('status-update-lit');
30
+ });
31
+
32
+ jQuery("#status_update_form").submit(function() {
33
+ jQuery("#status-update-field").hide();
34
+ jQuery("#submit_status").hide();
35
+ jQuery("#progress-bar").show();
36
+ });
37
+ });
38
+ <% end -%>
@@ -0,0 +1,6 @@
1
+ <div class="activity-status-update delete-container" id="<%= activity.dom_id %>">
2
+ <%= icon activity.source %> <%= link_to activity.source.display_name, activity.source %> <%= activity.content %>
3
+ <span class="activity-time"><%= t("muck.activities.time_ago", :time_in_words => time_ago_in_words(activity.created_at)) %></span>
4
+ <%= delete_activity(activity, :image) %>
5
+ <div class="clear"></div>
6
+ </div>
@@ -0,0 +1,3 @@
1
+ ActionController::Routing::Routes.draw do |map|
2
+ map.resources :activities, :controller => 'muck/activities'
3
+ end
@@ -0,0 +1,36 @@
1
+ class AddMuckActivities < ActiveRecord::Migration
2
+
3
+ def self.up
4
+
5
+ create_table :activities, :force => true do |t|
6
+ t.integer :item_id
7
+ t.string :item_type
8
+ t.string :template
9
+ t.integer :source_id
10
+ t.string :source_type
11
+ t.text :content
12
+ t.string :title
13
+ t.boolean :is_status_update, :default => false
14
+ t.boolean :is_public, :default => true
15
+ t.timestamps
16
+ end
17
+
18
+ add_index :activities, ["item_id", "item_type"]
19
+
20
+ create_table :activity_feeds, :force => true do |t|
21
+ t.integer :activity_id
22
+ t.integer :ownable_id
23
+ t.string :ownable_type
24
+ end
25
+
26
+ add_index :activity_feeds, ["activity_id"]
27
+ add_index :activity_feeds, ["ownable_id", "ownable_type"]
28
+
29
+ end
30
+
31
+ def self.down
32
+ drop_table :activities
33
+ drop_table :activity_feeds
34
+ end
35
+
36
+ end
data/install.rb ADDED
@@ -0,0 +1 @@
1
+ # Install hook code here
@@ -0,0 +1,8 @@
1
+ class ActionController::Routing::RouteSet
2
+ def load_routes_with_muck_activity!
3
+ muck_activity_routes = File.join(File.dirname(__FILE__), *%w[.. .. config muck_activity_routes.rb])
4
+ add_configuration_file(muck_activity_routes) unless configuration_files.include? muck_activity_routes
5
+ load_routes_without_muck_activity!
6
+ end
7
+ alias_method_chain :load_routes!, :muck_activity
8
+ end
@@ -0,0 +1,28 @@
1
+ require 'rake'
2
+ require 'rake/tasklib'
3
+ require 'fileutils'
4
+
5
+ module MuckActivity
6
+ class Tasks < ::Rake::TaskLib
7
+ def initialize
8
+ define
9
+ end
10
+
11
+ private
12
+ def define
13
+
14
+ namespace :muck do
15
+ namespace :activity do
16
+ desc "Sync required files from activity."
17
+ task :sync do
18
+ path = File.join(File.dirname(__FILE__), *%w[.. ..])
19
+ system "rsync -ruv #{path}/db ."
20
+ system "rsync -ruv #{path}/public ."
21
+ end
22
+ end
23
+ end
24
+
25
+ end
26
+ end
27
+ end
28
+ MuckActivity::Tasks.new
@@ -0,0 +1,76 @@
1
+ require 'cgi'
2
+
3
+ module MuckActivity # :nodoc:
4
+
5
+ def self.included(base) # :nodoc:
6
+ base.extend ActMethods
7
+ end
8
+
9
+ module ActMethods
10
+
11
+ # +has_activities+ gives the class it is called on an activity feed and a method called
12
+ # +add_activity+ that can add activities into a feed. Retrieve activity feed items
13
+ # via object.activities. ie @user.activities.
14
+ def has_activities
15
+ unless included_modules.include? InstanceMethods
16
+ has_many :activity_feeds, :as => :ownable
17
+ has_many :activities, :through => :activity_feeds, :order => 'created_at desc'
18
+ include InstanceMethods
19
+ end
20
+ end
21
+
22
+ # +acts_as_activity_source+ gives the class it is called on a method called
23
+ # +add_activity+ that can add activities into a feed.
24
+ def acts_as_activity_source
25
+ unless included_modules.include? InstanceMethods
26
+ include InstanceMethods
27
+ end
28
+ end
29
+
30
+ end
31
+
32
+ module InstanceMethods
33
+
34
+ # +add_activity+ adds an activity to all activites feeds that belong to the objects found in feed_to.
35
+ # * +feed_to+: an array of objects that have +has_activities+ declared on them. The generated activity
36
+ # will be pushed into the feed of each of these objects.
37
+ # * +source+: the object that peformed the activity ie a user or group
38
+ # * +item+: an object that will be used to generated the entry in an activity feed
39
+ # * +template+: name of an partial that will be used to generated the entry in the activity feed. Place
40
+ # templates in /app/views/activity_templates
41
+ # * +title+: optional title that can be used in the template
42
+ # * +content+: option content that can be used in the template. Useful for activities that might not have
43
+ # an item but instead might have a message or other text.
44
+ # * +check_method+: method that will be called on each item in the feed_to array. If the method evaluates
45
+ # to false the activity won't be added to the object's activity feed. An example usage would be
46
+ # letting users configure which items they want to have in their activity feed.
47
+ def add_activity(feed_to, source, item, template, title = '', content = '', check_method = nil)
48
+ feed_to = [feed_to] unless feed_to.is_a?(Array)
49
+ activity = Activity.create(:item => item, :source => source, :template => template, :title => title, :content => content)
50
+ feed_to.each do |ft|
51
+ if check_method
52
+ ft.activities << activity if ft.send(check_method)
53
+ else
54
+ ft.activities << activity
55
+ end
56
+ end
57
+ end
58
+
59
+ # +status+ returns the first activity item from the user's activity feed that is a status update.
60
+ # Used for displaying the last status update the user made
61
+ def status
62
+ self.activities.find(:first, :conditions => ['is_status_update = true'], :order => 'created_at DESC')
63
+ end
64
+
65
+ def can_view?(check_object)
66
+ self == check_object
67
+ end
68
+ end
69
+
70
+ end
71
+
72
+ ActiveRecord::Base.send(:include, MuckActivity)
73
+ ActionController::Base.send(:include, MuckActivity)
74
+ ActionController::Base.send :helper, MuckActivityHelper
75
+
76
+ I18n.load_path += Dir[ File.join(File.dirname(__FILE__), '..', 'locales', '*.{rb,yml}') ]
data/locales/en.yml ADDED
@@ -0,0 +1,22 @@
1
+ en:
2
+ muck:
3
+ activities:
4
+ item_removed: 'Item successfully removed from the recent activities list.'
5
+ item_could_not_be_removed: 'Item could not be removed from the recent activities list.'
6
+ item_created: 'Added activity'
7
+ permission_denied: "Sorry, you can't do that."
8
+ status_update_prompt: "What are you doing right now?"
9
+ activity_feed_title: 'Recent Activity'
10
+ updating_status_message: "Updating your status. Please wait."
11
+ status_indicator: is
12
+ invite_friends: Invite Friends
13
+ joined_status: "{{name}} joined {{application_name}}"
14
+ post: Share
15
+ clear: Clear
16
+ problem_with_status: "There was a problem changing your status"
17
+ update_error: "Oops... There was a problem. {{errors}}"
18
+ update_status_message: "update your status!"
19
+ time_ago: "{{time_in_words}} ago"
20
+ template_or_item_required: "An activity requires a template or an item to display correctly"
21
+ paging_newer: '&laquo; Newer'
22
+ paging_older: 'Older &raquo;'
Binary file
data/rails/init.rb ADDED
@@ -0,0 +1,4 @@
1
+ ActiveSupport::Dependencies.load_once_paths << lib_path
2
+
3
+ require 'muck_activity'
4
+ require 'muck_activity/initialize_routes'
@@ -0,0 +1 @@
1
+ require File.join(File.dirname(__FILE__), '/../lib/muck_activity/tasks')
data/test/factories.rb ADDED
@@ -0,0 +1,41 @@
1
+ Factory.sequence :email do |n|
2
+ "somebody#{n}@example.com"
3
+ end
4
+
5
+ Factory.sequence :login do |n|
6
+ "inquire#{n}"
7
+ end
8
+
9
+ Factory.sequence :name do |n|
10
+ "a_name#{n}"
11
+ end
12
+
13
+ Factory.sequence :abbr do |n|
14
+ "abbr#{n}"
15
+ end
16
+
17
+ Factory.sequence :description do |n|
18
+ "This is the description: #{n}"
19
+ end
20
+
21
+ Factory.define :user do |f|
22
+ f.login { Factory.next(:login) }
23
+ f.email { Factory.next(:email) }
24
+ f.password 'inquire_pass'
25
+ f.password_confirmation 'inquire_pass'
26
+ f.first_name 'test'
27
+ f.last_name 'guy'
28
+ f.terms_of_service true
29
+ f.activated_at DateTime.now
30
+ end
31
+
32
+ Factory.define :activity do |f|
33
+ f.item {|a| a.association(:user)}
34
+ f.template ''
35
+ f.source {|a| a.association(:user)}
36
+ f.content ''
37
+ f.title ''
38
+ f.is_status_update false
39
+ f.is_public true
40
+ f.created_at DateTime.now
41
+ end
@@ -0,0 +1,64 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+
3
+ class Muck::ActivitiesControllerTest < ActionController::TestCase
4
+
5
+ tests Muck::ActivitiesController
6
+
7
+ context "activities controller" do
8
+ setup do
9
+ activate_authlogic
10
+ @user = Factory(:user)
11
+ login_as @user
12
+ end
13
+
14
+ context 'on GET to index (js)' do
15
+ setup do
16
+ @activity = Factory(:activity)
17
+ get :index, :parent_type => @user.type, :parent_id => @user, :format => 'js', :latest_activity_id => @activity.to_param
18
+ end
19
+ should_respond_with :success
20
+ end
21
+
22
+ context 'on GET to index (js) no latest activity id' do
23
+ setup do
24
+ get :index, :parent_type => @user.type, :parent_id => @user, :format => 'js', :latest_activity_id => nil
25
+ end
26
+ should_respond_with :success
27
+ end
28
+
29
+ context 'on POST to create (js)' do
30
+ setup do
31
+ post :create, :activity => { :content => 'test activity' }, :parent_type => @user.type, :parent_id => @user, :format => 'js'
32
+ end
33
+ should_respond_with :success
34
+ should_not_set_the_flash
35
+ end
36
+
37
+ context 'on POST to create' do
38
+ setup do
39
+ post :create, :activity => { :content => 'test activity' }, :parent_type => @user.type, :parent_id => @user
40
+ end
41
+ should_redirect_to('show user page (user dashboard)') { @user }
42
+ end
43
+
44
+ context 'on DELETE to destroy' do
45
+ setup do
46
+ @activity = Factory(:activity, :source => @user)
47
+ delete :destroy, :id => @activity.id
48
+ end
49
+ should_respond_with :redirect
50
+ should_set_the_flash_to(I18n.t("muck.activities.item_removed"))
51
+ end
52
+
53
+ context 'on DELETE to destroy (js)' do
54
+ setup do
55
+ @activity = Factory(:activity, :source => @user)
56
+ delete :destroy, :id => @activity.id, :format => 'js'
57
+ end
58
+ should_respond_with :success
59
+ should_not_set_the_flash
60
+ end
61
+
62
+ end
63
+
64
+ end
@@ -0,0 +1,8 @@
1
+ require 'test_helper'
2
+
3
+ class MuckActivityTest < ActiveSupport::TestCase
4
+ # Replace this with your real tests.
5
+ test "the truth" do
6
+ assert true
7
+ end
8
+ end
@@ -0,0 +1,43 @@
1
+ ActiveSupport::TestCase.class_eval do
2
+
3
+ def self.should_require_login(*args)
4
+ args = Hash[*args]
5
+ login_url = args.delete :login_url
6
+ args.each do |action, verb|
7
+ should "Require login for '#{action}' action" do
8
+ send(verb, action)
9
+ assert_redirected_to(login_url)
10
+ end
11
+ end
12
+ end
13
+
14
+ def self.should_require_role(role, redirect_url, *actions)
15
+ actions.each do |action|
16
+ should "require role for '#{action}' action" do
17
+ get(action)
18
+ ensure_flash(/permission/i)
19
+ assert_response :redirect
20
+ end
21
+ end
22
+ end
23
+
24
+ #from: http://blog.internautdesign.com/2008/9/11/more-on-custom-shoulda-macros-scoping-of-instance-variables
25
+ def self.should_not_allow action, object, url= "/login", msg=nil
26
+ msg ||= "a #{object.class.to_s.downcase}"
27
+ should "not be able to #{action} #{msg}" do
28
+ object = eval(object, self.send(:binding), __FILE__, __LINE__)
29
+ get action, :id => object.id
30
+ assert_redirected_to url
31
+ end
32
+ end
33
+
34
+ def self.should_allow action, object, msg=nil
35
+ msg ||= "a #{object.class.to_s.downcase}"
36
+ should "be able to #{action} #{msg}" do
37
+ object = eval(object, self.send(:binding), __FILE__, __LINE__)
38
+ get action, :id => object.id
39
+ assert_response :success
40
+ end
41
+ end
42
+
43
+ end
@@ -0,0 +1,35 @@
1
+ $:.reject! { |e| e.include? 'TextMate' }
2
+ ENV["RAILS_ENV"] = "test"
3
+ #require File.expand_path(File.dirname(__FILE__) + "/../../../../config/environment")
4
+ require 'factory_girl'
5
+ require 'ruby-debug'
6
+ require 'mocha'
7
+ require 'authlogic/test_case'
8
+ require 'redgreen' rescue LoadError
9
+ require File.expand_path(File.dirname(__FILE__) + '/factories')
10
+ require File.join(File.dirname(__FILE__), 'shoulda_macros', 'controller')
11
+ class ActiveSupport::TestCase
12
+ self.use_transactional_fixtures = true
13
+ self.use_instantiated_fixtures = false
14
+
15
+ include Authlogic::TestCase
16
+
17
+ def login_as(user)
18
+ success = UserSession.create(user)
19
+ if !success
20
+ errors = user.errors.full_messages.to_sentence
21
+ message = 'User has not been activated' if !user.active?
22
+ raise "could not login as #{user.to_param}. Please make sure the user is valid. #{message} #{errors}"
23
+ end
24
+ UserSession.find
25
+ end
26
+
27
+ def assure_logout
28
+ user_session = UserSession.find
29
+ user_session.destroy if user_session
30
+ end
31
+
32
+ def ensure_flash(val)
33
+ assert_contains flash.values, val, ", Flash: #{flash.inspect}"
34
+ end
35
+ end
@@ -0,0 +1,5 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+
3
+ class ActivityFeedTest < ActiveSupport::TestCase
4
+
5
+ end
@@ -0,0 +1,49 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+
3
+ class ActivityTest < ActiveSupport::TestCase
4
+
5
+ context 'An Activity' do
6
+
7
+ should_validate_presence_of :source
8
+
9
+ should_belong_to :item
10
+ should_belong_to :source
11
+ should_have_many :activity_feeds
12
+
13
+ should_have_named_scope :since
14
+ should_have_named_scope :before
15
+ should_have_named_scope :recent
16
+ should_have_named_scope :only_public
17
+
18
+ end
19
+
20
+ should "require template or item" do
21
+ activity = Factory.build(:activity, :template => nil, :item => nil)
22
+ assert !activity.valid?
23
+ end
24
+
25
+ should "get the partial from the template" do
26
+ template = 'status_update'
27
+ activity = Factory(:activity, :template => template, :item => nil)
28
+ assert activity.partial == template, "The activity partial was not set to the specified template"
29
+ end
30
+
31
+ should "get the partial from the item" do
32
+ user = Factory(:user)
33
+ activity = Factory(:activity, :item => user, :template => nil)
34
+ assert activity.partial == user.class.name.underscore
35
+ end
36
+
37
+ should "be able to edit the activity" do
38
+ user = Factory(:user)
39
+ activity = Factory(:activity, :source => user)
40
+ assert activity.can_edit?(user)
41
+ end
42
+
43
+ should "not be able to edit the activity" do
44
+ user = Factory(:user)
45
+ activity = Factory(:activity)
46
+ assert !activity.can_edit?(user)
47
+ end
48
+
49
+ end
data/uninstall.rb ADDED
@@ -0,0 +1 @@
1
+ # Uninstall hook code here
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: muck-activity
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Justin Ball
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-06-11 00:00:00 -06:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: muck-engine
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: muck-users
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "0"
34
+ version:
35
+ description: Activity engine for the muck system.
36
+ email: justinball@gmail.com
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - README.rdoc
43
+ files:
44
+ - MIT-LICENSE
45
+ - README.rdoc
46
+ - Rakefile
47
+ - VERSION
48
+ - app/controllers/muck/activities_controller.rb
49
+ - app/helpers/muck_activity_helper.rb
50
+ - app/models/activity.rb
51
+ - app/models/activity_feed.rb
52
+ - app/views/activities/_activity_feed.html.erb
53
+ - app/views/activities/_cached_activities.html.erb
54
+ - app/views/activities/_current_status.html.erb
55
+ - app/views/activities/_delete.html.erb
56
+ - app/views/activities/_status_update.html.erb
57
+ - app/views/activity_templates/_status_update.html.erb
58
+ - config/muck_activity_routes.rb
59
+ - db/migrate/20090402033319_add_muck_activities.rb
60
+ - install.rb
61
+ - lib/muck_activity.rb
62
+ - lib/muck_activity/initialize_routes.rb
63
+ - lib/muck_activity/tasks.rb
64
+ - locales/en.yml
65
+ - public/images/loading.gif
66
+ - rails/init.rb
67
+ - tasks/muck_activity_tasks.rake
68
+ - test/factories.rb
69
+ - test/functional/activities_controller_test.rb
70
+ - test/muck_activity_test.rb
71
+ - test/shoulda_macros/controller.rb
72
+ - test/test_helper.rb
73
+ - test/unit/activity_feed_test.rb
74
+ - test/unit/activity_test.rb
75
+ - uninstall.rb
76
+ has_rdoc: true
77
+ homepage: http://github.com/jbasdf/muck_activity
78
+ post_install_message:
79
+ rdoc_options:
80
+ - --charset=UTF-8
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: "0"
88
+ version:
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: "0"
94
+ version:
95
+ requirements: []
96
+
97
+ rubyforge_project: muck-activity
98
+ rubygems_version: 1.3.1
99
+ signing_key:
100
+ specification_version: 2
101
+ summary: Activity engine for the muck system
102
+ test_files:
103
+ - test/factories.rb
104
+ - test/functional/activities_controller_test.rb
105
+ - test/muck_activity_test.rb
106
+ - test/shoulda_macros/controller.rb
107
+ - test/test_helper.rb
108
+ - test/unit/activity_feed_test.rb
109
+ - test/unit/activity_test.rb