redis_timeline 0.2.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.
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