redis_timeline 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9aaa7d0f9b35e188af7a9ea8b270dd0350d90b64
4
+ data.tar.gz: 561aaaca0aa1a37a7f6532c1cc4aebd3cd741a35
5
+ SHA512:
6
+ metadata.gz: 1b2b9ecb0d0684fb36ebf820aee7123c5520230999fa5e26103b640253d783b36cffae0cafe07be25c5b8ac5dfd5bd53aa5285a94e56448a9b7a1ad3d4d1ac21
7
+ data.tar.gz: 425ca83ad5c6c877b17040331627050ffba8a2f2c02cfd993c3ec5f38785662866833fb6b04c11903c51507f24b8b7803e0272aecd51eccf713df389bd35f336
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,148 @@
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 Post < ActiveRecord::Base
24
+ include Timeline::Track
25
+
26
+ track :new_post
27
+ end
28
+
29
+ By default, track fires in the `after_create` callback of your model and uses `self` as the object and `creator` as the actor.
30
+
31
+ You can specify these options explicity...
32
+
33
+ class Comment < ActiveRecord::Base
34
+ include Timeline::Track
35
+ belongs_to :author, class_name: "User"
36
+ belongs_to :post
37
+
38
+ track :new_comment,
39
+ actor: :author,
40
+ followers: :post_participants,
41
+ object: [:body],
42
+ on: :update,
43
+ target: :post
44
+
45
+ delegate :participants, to: :post, prefix: true
46
+ end
47
+
48
+ Parameters
49
+ ----------
50
+
51
+ `track` accepts the following parameters...
52
+
53
+ the first param is the verb name.
54
+
55
+ The rest all fit neatly in an options hash.
56
+
57
+ * `on:` [ActiveModel callback]
58
+ You use it to specify whether you want the timeline activity created after a create, update or destroy.
59
+ Default: :create
60
+
61
+ * `actor:` [the method that specifies the object that took this action]
62
+ In the above example, comment.author is this object.
63
+ Default: :creator, so make sure this exists if you don't specify a method here
64
+
65
+ * `object:` defaults to self, which is good most of the time.
66
+ You can override it if you need to
67
+
68
+ * `target:` [related to the `:object` method above. In the example this is the post related to the comment]
69
+ default: nil
70
+
71
+ * `followers:` [who should see this story in their timeline. This references a method on the actor]
72
+ Defaults to the method `followers` defined by Timeline::Actor.
73
+
74
+ * `extra_fields:` [accepts an array of method names that you would like to cache the value of in your timeline]
75
+ Defaults to nil.
76
+
77
+ * `if:` symbol or proc/lambda lets you put conditions on when to track.
78
+
79
+ Display a timeline
80
+ ------------------
81
+
82
+ To retrieve a timeline for a user...
83
+
84
+ class User < ActiveRecord::Base
85
+ include Timeline::Actor
86
+ end
87
+
88
+ The timeline objects are just hashes that are extended by [Hashie](http://github.com/intridea/hashie) to provide method access to the keys.
89
+
90
+ user = User.find(1)
91
+ user.timeline # => [<Timeline::Activity verb='new_comment' ...>]
92
+
93
+ Requirements
94
+ ------------
95
+
96
+ * redis
97
+ * active_support
98
+ * hashie
99
+
100
+ Install
101
+ -------
102
+
103
+ Install redis.
104
+
105
+ Add to your Gemfile:
106
+
107
+ gem 'redis-timeline'
108
+
109
+ Or install it by hand:
110
+
111
+ gem install redis-timeline
112
+
113
+ Setup your redis instance. For a Rails app, something like this...
114
+
115
+ # in config/initializers/redis.rb
116
+
117
+ Timeline.redis = "localhost:6379/timeline"
118
+
119
+ Author
120
+ ------
121
+
122
+ Original author: Felix Clack
123
+
124
+ License
125
+ -------
126
+
127
+ (The MIT License)
128
+
129
+ Copyright (c) 2012 Felix Clack
130
+
131
+ Permission is hereby granted, free of charge, to any person obtaining
132
+ a copy of this software and associated documentation files (the
133
+ 'Software'), to deal in the Software without restriction, including
134
+ without limitation the rights to use, copy, modify, merge, publish,
135
+ distribute, sublicense, and/or sell copies of the Software, and to
136
+ permit persons to whom the Software is furnished to do so, subject to
137
+ the following conditions:
138
+
139
+ The above copyright notice and this permission notice shall be
140
+ included in all copies or substantial portions of the Software.
141
+
142
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
143
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
144
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
145
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
146
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
147
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
148
+ 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,14 @@
1
+ require 'active_support/concern'
2
+ require 'multi_json'
3
+ require 'hashie'
4
+ require 'timeline/config'
5
+ require 'timeline/helpers'
6
+ require 'timeline/track'
7
+ require 'timeline/actor'
8
+ require 'timeline/activity'
9
+
10
+ module Timeline
11
+ extend Config
12
+ extend Helpers
13
+ end
14
+
@@ -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,34 @@
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
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ 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,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,118 @@
1
+ module Timeline::Track
2
+ extend ActiveSupport::Concern
3
+
4
+ module ClassMethods
5
+ def track(name, options={})
6
+ @name = name
7
+ @callback = options.delete :on
8
+ @callback ||= :create
9
+ @actor = options.delete :actor
10
+ @actor ||= :creator
11
+ @object = options.delete :object
12
+ @target = options.delete :target
13
+ @followers = options.delete :followers
14
+ @followers ||= :followers
15
+ @mentionable = options.delete :mentionable
16
+
17
+ method_name = "track_#{@name}_after_#{@callback}".to_sym
18
+ define_activity_method method_name, actor: @actor, object: @object, target: @target, followers: @followers, verb: name, mentionable: @mentionable
19
+
20
+ send "after_#{@callback}".to_sym, method_name, if: options.delete(:if)
21
+ end
22
+
23
+ private
24
+ def define_activity_method(method_name, options={})
25
+ define_method method_name do
26
+ @actor = send(options[:actor])
27
+ @fields_for = {}
28
+ @object = set_object(options[:object])
29
+ @target = !options[:target].nil? ? send(options[:target].to_sym) : nil
30
+ @extra_fields ||= nil
31
+ @followers = @actor.send(options[:followers].to_sym)
32
+ @mentionable = options[:mentionable]
33
+ add_activity activity(verb: options[:verb])
34
+ end
35
+ end
36
+ end
37
+
38
+ protected
39
+ def activity(options={})
40
+ {
41
+ verb: options[:verb],
42
+ actor: options_for(@actor),
43
+ object: options_for(@object),
44
+ target: options_for(@target),
45
+ created_at: Time.now
46
+ }
47
+ end
48
+
49
+ def add_activity(activity_item)
50
+ redis_add "global:activity", activity_item
51
+ add_activity_to_user(activity_item[:actor][:id], activity_item)
52
+ add_activity_by_user(activity_item[:actor][:id], activity_item)
53
+ add_mentions(activity_item)
54
+ add_activity_to_followers(activity_item) if @followers.any?
55
+ end
56
+
57
+ def add_activity_by_user(user_id, activity_item)
58
+ redis_add "user:id:#{user_id}:posts", activity_item
59
+ end
60
+
61
+ def add_activity_to_user(user_id, activity_item)
62
+ redis_add "user:id:#{user_id}:activity", activity_item
63
+ end
64
+
65
+ def add_activity_to_followers(activity_item)
66
+ @followers.each { |follower| add_activity_to_user(follower.id, activity_item) }
67
+ end
68
+
69
+ def add_mentions(activity_item)
70
+ return unless @mentionable and @object.send(@mentionable)
71
+ @object.send(@mentionable).scan(/@\w+/).each do |mention|
72
+ if user = @actor.class.find_by_username(mention[1..-1])
73
+ add_mention_to_user(user.id, activity_item)
74
+ end
75
+ end
76
+ end
77
+
78
+ def add_mention_to_user(user_id, activity_item)
79
+ redis_add "user:id:#{user_id}:mentions", activity_item
80
+ end
81
+
82
+ def extra_fields_for(object)
83
+ return {} unless @fields_for.has_key?(object.class.to_s.downcase.to_sym)
84
+ @fields_for[object.class.to_s.downcase.to_sym].inject({}) do |sum, method|
85
+ sum[method.to_sym] = @object.send(method.to_sym)
86
+ sum
87
+ end
88
+ end
89
+
90
+ def options_for(target)
91
+ if !target.nil?
92
+ {
93
+ id: target.id,
94
+ class: target.class.to_s,
95
+ display_name: target.to_s
96
+ }.merge(extra_fields_for(target))
97
+ else
98
+ nil
99
+ end
100
+ end
101
+
102
+ def redis_add(list, activity_item)
103
+ Timeline.redis.lpush list, Timeline.encode(activity_item)
104
+ end
105
+
106
+ def set_object(object)
107
+ case
108
+ when object.is_a?(Symbol)
109
+ send(object)
110
+ when object.is_a?(Array)
111
+ @fields_for[self.class.to_s.downcase.to_sym] = object
112
+ self
113
+ else
114
+ self
115
+ end
116
+ end
117
+
118
+ end