paritytimeline 0.4

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 19db87a66812a801cff06086f49018f30f6133de
4
+ data.tar.gz: ba8b8ff4dbba4b63b7c3c55f10f77407792dc63c
5
+ SHA512:
6
+ metadata.gz: c213c9aa2bcedd59cbc3ad53dadb732e9125879d9ac71055ab9603ef23c2f4aaf54fadf78b79620cf78be97a31011c2c2d3f0b2d4a0a035bf71c302b1e3dc73e
7
+ data.tar.gz: c3784e1b7803c9e8fa67a89289295e20527316120cafbdf32a1621fbc97dd615cde30b12263911773e7aa2f6b880d73b6af2286c1918bcc80afad9ceb2ab8ce8
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2012 Felix Clack
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.md ADDED
@@ -0,0 +1,152 @@
1
+ redis-timeline
2
+ ===========
3
+
4
+ [![Build Status](https://travis-ci.org/felixclack/redis-timeline.png?branch=master)](https://travis-ci.org/felixclack/redis-timeline)
5
+
6
+ Redis backed timelines in your app.
7
+
8
+ <a href="mailto:felixclack+pairwithme@gmail.com" title="Pair program with me!">
9
+ <img src="http://pairprogramwith.me/badge.png"
10
+ alt="Pair program with me!" />
11
+ </a>
12
+
13
+ Features
14
+ --------
15
+
16
+ * store your timeline in Redis.
17
+
18
+ Examples
19
+ --------
20
+
21
+ The simple way...
22
+
23
+ class PostsController < ApplicationController
24
+ include Timeline::ControllerHelper
25
+
26
+ end
27
+
28
+ Instead doing on the callback we explicity mention when we want to track the activity
29
+
30
+ You can specify these options ...
31
+
32
+ class PostsController < ApplicationController
33
+ include Timeline::ControllerHelper
34
+ belongs_to :author, class_name: "User"
35
+ belongs_to :post
36
+
37
+ track :new_comment,
38
+ actor: :author,
39
+ followers: :post_participants,
40
+ object: [:body],
41
+ on: :update,
42
+ target: :post
43
+
44
+ delegate :participants, to: :post, prefix: true
45
+ def create
46
+ @post=Post.new(params[:post])
47
+ respond_to do |format|
48
+ if @post.save!
49
+ track_timeline_activity(:new_post,actor: current_user,followers: current_user.followers,target: @post.comment,object: @post)
50
+ format.html { redirect_to(posts_path , :notice => 'Post was successfully created.') }
51
+ else
52
+ format.html { redirect_to(posts_path , :notice => @post.errors.full_messages) }
53
+ format.json { render json: @post.errors, status: :unprocessable_entity }
54
+ end
55
+
56
+ end
57
+ end
58
+ end
59
+
60
+ Parameters
61
+ ----------
62
+
63
+ `track` accepts the following parameters...
64
+
65
+ the first param is the verb name.
66
+
67
+ The rest all fit neatly in an options hash.
68
+
69
+ * `actor:` [the method that specifies the object that took this action]
70
+ In the above example, comment.author is this object.
71
+
72
+
73
+ * `object:` defaults to self, which is good most of the time.
74
+ You can override it if you need to
75
+
76
+ * `target:` [related to the `:object` method above. In the example this is the post related to the comment]
77
+ default: nil
78
+
79
+ * `followers:` [who should see this story in their timeline. This references a method on the actor]
80
+ Defaults to the method `followers` defined by Timeline::Actor.
81
+
82
+
83
+ Display a timeline
84
+ ------------------
85
+
86
+ To retrieve a timeline for a user...
87
+
88
+ class User < ActiveRecord::Base
89
+ include Timeline::Actor
90
+ end
91
+
92
+ The timeline objects are just hashes that are extended by [Hashie](http://github.com/intridea/hashie) to provide method access to the keys.
93
+
94
+ user = User.find(1)
95
+ user.timeline # => [<Timeline::Activity verb='new_comment' ...>]
96
+
97
+ Requirements
98
+ ------------
99
+
100
+ * redis
101
+ * active_support
102
+ * hashie
103
+
104
+ Install
105
+ -------
106
+
107
+ Install redis.
108
+
109
+ Add to your Gemfile:
110
+
111
+ gem 'redis-timeline'
112
+
113
+ Or install it by hand:
114
+
115
+ gem install redis-timeline
116
+
117
+ Setup your redis instance. For a Rails app, something like this...
118
+
119
+ # in config/initializers/redis.rb
120
+
121
+ Timeline.redis = "localhost:6379/timeline"
122
+
123
+ Author
124
+ ------
125
+
126
+ Original author: Felix Clack
127
+
128
+ License
129
+ -------
130
+
131
+ (The MIT License)
132
+
133
+ Copyright (c) 2012 Felix Clack
134
+
135
+ Permission is hereby granted, free of charge, to any person obtaining
136
+ a copy of this software and associated documentation files (the
137
+ 'Software'), to deal in the Software without restriction, including
138
+ without limitation the rights to use, copy, modify, merge, publish,
139
+ distribute, sublicense, and/or sell copies of the Software, and to
140
+ permit persons to whom the Software is furnished to do so, subject to
141
+ the following conditions:
142
+
143
+ The above copyright notice and this permission notice shall be
144
+ included in all copies or substantial portions of the Software.
145
+
146
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
147
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
148
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
149
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
150
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
151
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
152
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+ begin
8
+ require 'rdoc/task'
9
+ rescue LoadError
10
+ require 'rdoc/rdoc'
11
+ require 'rake/rdoctask'
12
+ RDoc::Task = Rake::RDocTask
13
+ end
14
+
15
+ RDoc::Task.new(:rdoc) do |rdoc|
16
+ rdoc.rdoc_dir = 'rdoc'
17
+ rdoc.title = 'Timeline'
18
+ rdoc.options << '--line-numbers'
19
+ rdoc.rdoc_files.include('README.rdoc')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
22
+
23
+
24
+
25
+
26
+ Bundler::GemHelper.install_tasks
27
+
28
+ require 'rake/testtask'
29
+
30
+ Rake::TestTask.new(:test) do |t|
31
+ t.libs << 'lib'
32
+ t.libs << 'test'
33
+ t.pattern = 'test/**/*_test.rb'
34
+ t.verbose = false
35
+ end
36
+
37
+
38
+ task :default => :test
@@ -0,0 +1,21 @@
1
+ require 'active_support/concern'
2
+ require 'action_controller'
3
+ require 'multi_json'
4
+ require 'hashie'
5
+ require 'timeline/config'
6
+ require 'timeline/helpers'
7
+ require 'timeline/track'
8
+ require 'timeline/actor'
9
+ require 'timeline/activity'
10
+
11
+
12
+ module Timeline
13
+ extend Config
14
+ extend Helpers
15
+ end
16
+
17
+ require 'timeline/controller_helper'
18
+ require 'timeline/notification_helper'
19
+
20
+ ActionController::Base.send :include, Timeline::ControllerHelper
21
+ ActionController::Base.send :include, Timeline::NotificationHelper
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :timeline do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,11 @@
1
+ require 'active_model'
2
+
3
+ module Timeline
4
+ class Activity < Hashie::Mash
5
+ extend ActiveModel::Naming
6
+
7
+ def to_partial_path
8
+ "timelines/#{verb}"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,36 @@
1
+ module Timeline
2
+ module Actor
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ def timeline(options={})
7
+ ::Timeline.get_list(timeline_options(options)).map do |item|
8
+ ::Timeline::Activity.new ::Timeline.decode(item)
9
+ end
10
+ end
11
+
12
+ def followers
13
+ []
14
+ end
15
+
16
+ private
17
+ def timeline_options(options)
18
+ defaults = { list_name: "user:id:#{self.id}:activity", start: 0, end: 19 }
19
+ if options.is_a? Hash
20
+ defaults.merge!(options)
21
+ elsif options.is_a? Symbol
22
+ case options
23
+ when :global
24
+ defaults.merge!(list_name: "global:activity")
25
+ when :posts
26
+ defaults.merge!(list_name: "user:id:#{self.id}:posts")
27
+ when :mentions
28
+ defaults.merge!(list_name: "user:id:#{self.id}:mentions")
29
+ when :notifications
30
+ defaults.merge!(list_name: "user:id:#{self.id}:notification")
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,42 @@
1
+ require 'redis/namespace'
2
+
3
+ module Timeline
4
+ module Config
5
+
6
+ # Accepts:
7
+ # 1. A 'hostname:port' String
8
+ # 2. A 'hostname:port:db' String (to select the Redis db)
9
+ # 3. A 'hostname:port/namespace' String (to set the Redis namespace)
10
+ # 4. A Redis URL String 'redis://host:port'
11
+ # 5. An instance of `Redis`, `Redis::Client`, `Redis::DistRedis`,
12
+ # or `Redis::Namespace`.
13
+ def redis=(server)
14
+ case server
15
+ when String
16
+ if server =~ /redis\:\/\//
17
+ redis = Redis.connect(:url => server, :thread_safe => true)
18
+ else
19
+ server, namespace = server.split('/', 2)
20
+ host, port, db = server.split(':')
21
+ redis = Redis.new(:host => host, :port => port,
22
+ :thread_safe => true, :db => db)
23
+ end
24
+ namespace ||= :timeline
25
+
26
+ @redis = Redis::Namespace.new(namespace, :redis => redis)
27
+ when Redis::Namespace
28
+ @redis = server
29
+ else
30
+ @redis = Redis::Namespace.new(:timeline, :redis => server)
31
+ end
32
+ end
33
+
34
+ # Returns the current Redis connection. If none has been created, will
35
+ # create a new one.
36
+ def redis
37
+ return @redis if @redis
38
+ self.redis = Redis.respond_to?(:connect) ? Redis.connect : "localhost:6379"
39
+ self.redis
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,153 @@
1
+ module Timeline
2
+
3
+ module ControllerHelper
4
+ def self.included(base)
5
+ base.send :include, InstanceMethods
6
+ end
7
+
8
+ module InstanceMethods
9
+ def track_timeline_activity(name, options={})
10
+ @name = name
11
+ @start_value = 0
12
+ @limit_records = (options[:limit_records] && options[:limit_records] > 30) ? options[:limit_records] : 30
13
+ @limit_days = (options[:limit_days] && options[:limit_days] > 30) ? options[:limit_days] : 30
14
+ @actor = options.delete :actor
15
+ @actor ||= :creator
16
+ @object = options.delete :object
17
+ @target = options.delete :target
18
+ @followers = options.delete :followers
19
+ @friends = options.delete :friends
20
+ @mentionable = options.delete :mentionable
21
+
22
+ @fields_for = {}
23
+ @extra_fields ||= nil
24
+ @merge_similar = options[:merge_similar] == true ? true : false
25
+ options[:verb] = name
26
+
27
+ add_activity(activity(verb: options[:verb]))
28
+ end
29
+
30
+ # track_timeline_activity(:new_coupon,actor: :user,object: :coupon_code,followers: :followers)
31
+
32
+ protected
33
+
34
+ def activity(options={})
35
+ {
36
+ verb: options[:verb],
37
+ actor: options_for(@actor),
38
+ object: options_for(@object),
39
+ target: options_for(@target),
40
+ created_at: Time.now
41
+ }
42
+ end
43
+
44
+ def add_activity(activity_item)
45
+ redis_add "global:activity", activity_item
46
+ redis_add "global:activity:#{activity_item[:verb]}", activity_item
47
+ add_activity_to_user(activity_item[:actor][:id], activity_item)
48
+ add_activity_by_user(activity_item[:actor][:id], activity_item)
49
+ add_mentions(activity_item)
50
+ add_activity_to_followers(activity_item) if @followers.any?
51
+ add_activity_to_friends(activity_item) if @friends.any?
52
+ end
53
+
54
+ def add_activity_by_user(user_id, activity_item)
55
+ redis_add "user:id:#{user_id}:posts", activity_item
56
+ end
57
+
58
+ def add_activity_to_user(user_id, activity_item)
59
+ redis_add "user:id:#{user_id}:activity", activity_item
60
+ end
61
+
62
+ def add_activity_to_users_friends(user_id, activity_item)
63
+ redis_add "user:id:#{user_id}:activity:friends", activity_item
64
+ end
65
+
66
+ def add_activity_to_followers(activity_item)
67
+ @followers.each { |follower| add_activity_to_user(follower.id, activity_item) }
68
+ end
69
+
70
+ def add_activity_to_friends(activity_item)
71
+ @friends.each { |friend| add_activity_to_users_friends(friend, activity_item) }
72
+ end
73
+
74
+ def add_mentions(activity_item)
75
+ return unless @mentionable and @object.send(@mentionable)
76
+ @object.send(@mentionable).scan(/@\w+/).each do |mention|
77
+ if user = @actor.class.find_by_username(mention[1..-1])
78
+ add_mention_to_user(user.id, activity_item)
79
+ end
80
+ end
81
+ end
82
+
83
+ def add_mention_to_user(user_id, activity_item)
84
+ redis_add "user:id:#{user_id}:mentions", activity_item
85
+ end
86
+
87
+ def extra_fields_for(object)
88
+ return {} unless @fields_for.has_key?(object.class.to_s.downcase.to_sym)
89
+ @fields_for[object.class.to_s.downcase.to_sym].inject({}) do |sum, method|
90
+ sum[method.to_sym] = @object.send(method.to_sym)
91
+ sum
92
+ end
93
+ end
94
+
95
+ def options_for(target)
96
+ if !target.nil?
97
+ {
98
+ id: target.id,
99
+ class: target.class.to_s,
100
+ display_name: target.to_s
101
+ }.merge(extra_fields_for(target))
102
+ else
103
+ nil
104
+ end
105
+ end
106
+
107
+ def redis_add(list, activity_item)
108
+ Timeline.redis.lpush list, Timeline.encode(activity_item)
109
+ trim_activities list
110
+ end
111
+
112
+ def trim_activities(list)
113
+ return if (Timeline.redis.llen list) < @limit_records
114
+ last_record = get_record(list, -1)
115
+ return if (last_record && last_record["created_at"]) > Time.now - @limit_days.days
116
+ trim_old_activities(list)
117
+ end
118
+
119
+ def get_record(list, index)
120
+ last_record = Timeline.redis.lindex list, index
121
+ last_record = Timeline.decode(last_record) if last_record
122
+ end
123
+
124
+ def trim_old_activities(list)
125
+ Timeline.redis.ltrim list , 0 , get_trim_index(list, (Timeline.redis.llen list) - 1)
126
+ end
127
+
128
+ def get_trim_index(list, index)
129
+ return @limit_records if index < @limit_records
130
+ record = get_record(list, index)
131
+ if record && (record["created_at"] < Time.now - @limit_days.days)
132
+ return get_trim_index(list, index - 5)
133
+ else
134
+ return index
135
+ end
136
+ end
137
+
138
+ def set_object(object)
139
+ case
140
+ when object.is_a?(Symbol)
141
+ send(object)
142
+ when object.is_a?(Array)
143
+ @fields_for[self.class.to_s.downcase.to_sym] = object
144
+ self
145
+ else
146
+ self
147
+ end
148
+ end
149
+ end
150
+ end
151
+
152
+
153
+ end
@@ -0,0 +1,23 @@
1
+ module Timeline
2
+ module Helpers
3
+ class DecodeException < StandardError; end
4
+
5
+ def encode(object)
6
+ ::MultiJson.encode(object)
7
+ end
8
+
9
+ def decode(object)
10
+ return unless object
11
+
12
+ begin
13
+ ::MultiJson.decode(object)
14
+ rescue ::MultiJson::DecodeError => e
15
+ raise DecodeException, e
16
+ end
17
+ end
18
+
19
+ def get_list(options={})
20
+ Timeline.redis.lrange options[:list_name], options[:start], options[:end]
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,98 @@
1
+ module Timeline
2
+
3
+ module NotificationHelper
4
+ def self.included(base)
5
+ base.send :include, InstanceMethods
6
+ end
7
+
8
+ module InstanceMethods
9
+ def track_notification(name, options={})
10
+ @name = name
11
+ @actor = options[:actor]
12
+ @object = options[:object]
13
+ @target = options[:target]
14
+ @followers = set_follower(options[:followers])
15
+ @mentionable = options[:mentionable]
16
+ @read = options[:read] || false
17
+
18
+ add_activity_to_subscribed_user(@followers,notification_activity) if @followers.present?
19
+ add_mentions(notification_activity)
20
+ end
21
+
22
+ def add_activity_to_subscribed_user(followers, activity_item)
23
+ followers.each do |follower|
24
+ add_to_redis "user:id:#{follower.id}:notification", activity_item
25
+ trim_notification "user:id:#{follower.id}:notification"
26
+ end
27
+ end
28
+
29
+ def add_to_redis(list, activity_item)
30
+ Timeline.redis.lpush list, Timeline.encode(activity_item)
31
+ end
32
+
33
+ def trim_notification(list)
34
+ Timeline.redis.ltrim list, 0, 29
35
+ end
36
+
37
+ def add_mentions(activity_item)
38
+ return unless @mentionable
39
+ @mentionable.each do |mention|
40
+ if user = @actor.class.where("coalesce(display_name, login) = ?",mention)
41
+ add_activity_to_subscribed_user(user, activity_item)
42
+ end
43
+ end
44
+ end
45
+
46
+ def set_as_read_notification(user, read, options= {})
47
+ notifications = get_unread_notification(user, options)
48
+ notifications.each do |index, notification|
49
+ Timeline.redis.lset("user:id:#{user.id}:notification",index, Timeline.encode(reset_read_activity(notification, read)))
50
+ end
51
+ end
52
+
53
+ def get_unread_notification(user, options= {})
54
+ result = {}
55
+ Timeline.redis.lrange("user:id:#{user.id}:notification", options[:start] || 0, options[:end] || 10).each_with_index do |item, index|
56
+ data = Timeline.decode(item)
57
+ result.merge!(index => data) unless data["read"]
58
+ end
59
+ result
60
+ end
61
+
62
+
63
+ private
64
+ def notification_activity
65
+ {
66
+ verb: @name,
67
+ actor: @actor,
68
+ object: @object,
69
+ target: @target,
70
+ created_at: Time.now,
71
+ read: @read
72
+ }
73
+ end
74
+
75
+ def set_follower(follower)
76
+ if follower.is_a?(Array)
77
+ follower
78
+ elsif follower.present?
79
+ [follower]
80
+ else
81
+ []
82
+ end
83
+ end
84
+
85
+ def reset_read_activity(activity, read)
86
+ {
87
+ verb: activity["verb"],
88
+ actor: activity["actor"],
89
+ object: activity["object"],
90
+ target: activity["target"],
91
+ created_at: activity["created_at"],
92
+ read: read
93
+ }
94
+ end
95
+
96
+ end
97
+ end
98
+ end