redis-timelineglobal 0.1.7

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 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,146 @@
1
+ redis-timeline
2
+ ===========
3
+
4
+ Redis backed timelines in your app.
5
+
6
+ Features
7
+ --------
8
+
9
+ * store your timeline in Redis.
10
+
11
+ Examples
12
+ --------
13
+
14
+ The simple way...
15
+
16
+ class Post < ActiveRecord::Base
17
+ include Timeline::Track
18
+
19
+ track :new_post
20
+
21
+ end
22
+
23
+ By default, track fires in the `after_create` callback of your model and uses `self` as the object and `creator` as the actor.
24
+
25
+ You can specify these options explicity...
26
+
27
+ class Comment < ActiveRecord::Base
28
+ include Timeline::Track
29
+ belongs_to :author, class_name: "User"
30
+ belongs_to :post
31
+
32
+ track :new_comment,
33
+ on: :update,
34
+ actor: :author,
35
+ target: :post,
36
+ object: [:body]
37
+ followers: :post_participants
38
+
39
+ delegate :participants, to: :post, prefix: true
40
+ end
41
+
42
+ Parameters
43
+ ----------
44
+
45
+ `track` accepts the following parameters...
46
+
47
+ the first param is the verb name.
48
+
49
+ The rest all fit neatly in an options hash.
50
+
51
+ * `on:` [ActiveModel callback]
52
+ You use it to specify whether you want the timeline activity created after a create, update or destroy.
53
+ Default: :create
54
+
55
+ * `actor:` [the method that specifies the object that took this action]
56
+ In the above example, comment.author is this object.
57
+ Default: :creator, so make sure this exists if you don't specify a method here
58
+
59
+ * `object:` defaults to self, which is good most of the time.
60
+ You can override it if you need to
61
+
62
+ * `target:` [related to the `:object` method above. In the example this is the post related to the comment]
63
+ default: nil
64
+
65
+ * `followers:` [who should see this story in their timeline. This references a method on the actor]
66
+ Defaults to the method `followers` defined by Timeline::Actor.
67
+
68
+ * `extra_fields:` [accepts an array of method names that you would like to cache the value of in your timeline]
69
+ Defaults to nil.
70
+
71
+ * `merge_similar:` [allow merge similar feed as one]
72
+ For example, user upload some photos use multi uploader feature, you may need only on feed in timeline, so you need turn this on, then merged item will fix `object` as an Array type.
73
+ Default: false
74
+
75
+ * `if:` symbol or proc/lambda lets you put conditions on when to track.
76
+
77
+ Display a timeline
78
+ ------------------
79
+
80
+ To retrieve a timeline for a user...
81
+
82
+ class User < ActiveRecord::Base
83
+ include Timeline::Actor
84
+ end
85
+
86
+ The timeline objects are just hashes that are extended by [Hashie](http://github.com/intridea/hashie) to provide method access to the keys.
87
+
88
+ user = User.find(1)
89
+ user.timeline # => [<Timeline::Activity verb='new_comment' ...>]
90
+
91
+ Requirements
92
+ ------------
93
+
94
+ * redis
95
+ * active_support
96
+ * hashie
97
+
98
+ Install
99
+ -------
100
+
101
+ Install redis.
102
+
103
+ Add to your Gemfile:
104
+
105
+ gem 'redis-timeline'
106
+
107
+ Or install it by hand:
108
+
109
+ gem install redis-timeline
110
+
111
+ Setup your redis instance. For a Rails app, something like this...
112
+
113
+ # in config/initializers/redis.rb
114
+
115
+ Timeline.redis = "localhost:6379/timeline"
116
+
117
+ Author
118
+ ------
119
+
120
+ Original author: Felix Clack
121
+
122
+ License
123
+ -------
124
+
125
+ (The MIT License)
126
+
127
+ Copyright (c) 2012 Felix Clack
128
+
129
+ Permission is hereby granted, free of charge, to any person obtaining
130
+ a copy of this software and associated documentation files (the
131
+ 'Software'), to deal in the Software without restriction, including
132
+ without limitation the rights to use, copy, modify, merge, publish,
133
+ distribute, sublicense, and/or sell copies of the Software, and to
134
+ permit persons to whom the Software is furnished to do so, subject to
135
+ the following conditions:
136
+
137
+ The above copyright notice and this permission notice shall be
138
+ included in all copies or substantial portions of the Software.
139
+
140
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
141
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
142
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
143
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
144
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
145
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
146
+ 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'
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,32 @@
1
+ module Timeline::Actor
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ def timeline(options={})
6
+ Timeline.get_list(timeline_options(options)).map do |item|
7
+ Timeline::Activity.new Timeline.decode(item)
8
+ end
9
+ end
10
+
11
+ def followers
12
+ []
13
+ end
14
+
15
+ private
16
+ def timeline_options(options)
17
+ defaults = { list_name: "user:id:#{self.id}:activity", start: 0, end: 19 }
18
+ if options.is_a? Hash
19
+ defaults.merge!(options)
20
+ elsif options.is_a? Symbol
21
+ case options
22
+ when :global
23
+ defaults.merge!(list_name: "global:activity")
24
+ when :posts
25
+ defaults.merge!(list_name: "user:id:#{self.id}:posts")
26
+ when :mentions
27
+ defaults.merge!(list_name: "user:id:#{self.id}:mentions")
28
+ end
29
+ end
30
+ end
31
+ end
32
+ 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(:resque, :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,37 @@
1
+ require 'multi_json/version'
2
+
3
+ module Timeline
4
+ module Helpers
5
+ class DecodeException < StandardError; end
6
+
7
+ def encode(object)
8
+ if ::MultiJson::VERSION.to_f > 1.3
9
+ ::MultiJson.dump(object)
10
+ else
11
+ ::MultiJson.encode(object)
12
+ end
13
+ end
14
+
15
+ def decode(object)
16
+ return unless object
17
+
18
+ begin
19
+ if ::MultiJson::VERSION.to_f > 1.3
20
+ ::MultiJson.load(object)
21
+ else
22
+ ::MultiJson.decode(object)
23
+ end
24
+ rescue ::MultiJson::DecodeError => e
25
+ raise DecodeException, e
26
+ end
27
+ end
28
+
29
+ def get_list(options={})
30
+ keys = Timeline.redis.lrange options[:list_name], options[:start], options[:end]
31
+ return [] if keys.blank?
32
+ items = Timeline.redis.mget(*keys)
33
+ items.delete(nil)
34
+ items
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,147 @@
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,
19
+ object: @object,
20
+ target: @target,
21
+ followers: @followers,
22
+ verb: name,
23
+ merge_similar: options[:merge_similar],
24
+ mentionable: @mentionable
25
+
26
+ send "after_#{@callback}".to_sym, method_name, if: options.delete(:if)
27
+ end
28
+
29
+ private
30
+ def define_activity_method(method_name, options={})
31
+ define_method method_name do
32
+ @actor = send(options[:actor])
33
+ @fields_for = {}
34
+ @object = set_object(options[:object])
35
+ @target = !options[:target].nil? ? send(options[:target].to_sym) : nil
36
+ @extra_fields ||= nil
37
+ @merge_similar = options[:merge_similar] == true ? true : false
38
+ @followers = @actor.send(options[:followers].to_sym)
39
+ @mentionable = options[:mentionable]
40
+ add_activity(activity(verb: options[:verb]))
41
+ end
42
+ end
43
+ end
44
+
45
+ protected
46
+ def activity(options={})
47
+ {
48
+ cache_key: "#{options[:verb]}_u#{@actor.id}_o#{@object.id}_#{Time.now.to_i}",
49
+ verb: options[:verb],
50
+ actor: options_for(@actor),
51
+ object: options_for(@object),
52
+ target: options_for(@target),
53
+ created_at: Time.now
54
+ }
55
+ end
56
+
57
+ def add_activity(activity_item)
58
+ redis_store_item(activity_item)
59
+ add_activity_by_global(activity_item)
60
+ add_activity_to_user(activity_item[:actor][:id], activity_item)
61
+ add_activity_by_user(activity_item[:actor][:id], activity_item)
62
+ add_mentions(activity_item)
63
+ add_activity_to_followers(activity_item) if @followers.any?
64
+ end
65
+
66
+ def add_activity_by_global(activity_item)
67
+ redis_add "global:activity", activity_item
68
+ end
69
+
70
+ def add_activity_by_user(user_id, activity_item)
71
+ redis_add "user:id:#{user_id}:posts", activity_item
72
+ end
73
+
74
+ def add_activity_to_user(user_id, activity_item)
75
+ redis_add "user:id:#{user_id}:activity", activity_item
76
+ end
77
+
78
+ def add_activity_to_followers(activity_item)
79
+ @followers.each { |follower| add_activity_to_user(follower.id, activity_item) }
80
+ end
81
+
82
+ def add_mentions(activity_item)
83
+ return unless @mentionable and @object.send(@mentionable)
84
+ @object.send(@mentionable).scan(/@\w+/).each do |mention|
85
+ if user = @actor.class.find_by_username(mention[1..-1])
86
+ add_mention_to_user(user.id, activity_item)
87
+ end
88
+ end
89
+ end
90
+
91
+ def add_mention_to_user(user_id, activity_item)
92
+ redis_add "user:id:#{user_id}:mentions", activity_item
93
+ end
94
+
95
+ def extra_fields_for(object)
96
+ return {} unless @fields_for.has_key?(object.class.to_s.downcase.to_sym)
97
+ @fields_for[object.class.to_s.downcase.to_sym].inject({}) do |sum, method|
98
+ sum[method.to_sym] = @object.send(method.to_sym)
99
+ sum
100
+ end
101
+ end
102
+
103
+ def options_for(target)
104
+ if !target.nil?
105
+ {
106
+ id: target.id,
107
+ class: target.class.to_s,
108
+ display_name: target.to_s
109
+ }.merge(extra_fields_for(target))
110
+ else
111
+ nil
112
+ end
113
+ end
114
+
115
+ def redis_add(list, activity_item)
116
+ Timeline.redis.lpush list, activity_item[:cache_key]
117
+ end
118
+
119
+ def redis_store_item(activity_item)
120
+ if @merge_similar
121
+ # Merge similar item with last
122
+ last_item_text = Timeline.get_list(:list_name => "user:id:#{activity_item[:actor][:id]}:posts", :start => 0, :end => 1).first
123
+ if last_item_text
124
+ last_item = Timeline::Activity.new Timeline.decode(last_item_text)
125
+ if last_item[:verb].to_s == activity_item[:verb].to_s and last_item[:target] == activity_item[:target]
126
+ activity_item[:object] = [last_item[:object], activity_item[:object]].flatten.uniq
127
+ end
128
+ # Remove last similar item, it will merge to new item
129
+ Timeline.redis.del last_item[:cache_key]
130
+ end
131
+ end
132
+ Timeline.redis.set activity_item[:cache_key], Timeline.encode(activity_item)
133
+ end
134
+
135
+ def set_object(object)
136
+ case
137
+ when object.is_a?(Symbol)
138
+ send(object)
139
+ when object.is_a?(Array)
140
+ @fields_for[self.class.to_s.downcase.to_sym] = object
141
+ self
142
+ else
143
+ self
144
+ end
145
+ end
146
+
147
+ end
@@ -0,0 +1,3 @@
1
+ module Timeline
2
+ VERSION = "0.1.7"
3
+ end
@@ -0,0 +1,12 @@
1
+ require File.join(File.dirname(__FILE__), %w[spec_helper])
2
+
3
+ describe Timeline::Activity do
4
+ describe "initialized with json" do
5
+ let(:json) { { id: "1", verb: "new_post"} }
6
+
7
+ it "returns a Hashie-fied object" do
8
+ Timeline::Activity.new(json).id.should == "1"
9
+ Timeline::Activity.new(json).verb.should == "new_post"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,17 @@
1
+ require File.join(File.dirname(__FILE__), %w[spec_helper])
2
+
3
+ describe Timeline::Actor do
4
+ describe "when included" do
5
+ before { @user = User.new }
6
+
7
+ it "defines a timeline association" do
8
+ @user.should respond_to :timeline
9
+ end
10
+
11
+ describe ".timeline" do
12
+ subject { @user.timeline }
13
+
14
+ it { should == [] }
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,115 @@
1
+ # Redis configuration file example
2
+
3
+ # By default Redis does not run as a daemon. Use 'yes' if you need it.
4
+ # Note that Redis will write a pid file in /var/run/redis.pid when daemonized.
5
+ daemonize yes
6
+
7
+ # When run as a daemon, Redis write a pid file in /var/run/redis.pid by default.
8
+ # You can specify a custom pid file location here.
9
+ pidfile ./spec/redis-test.pid
10
+
11
+ # Accept connections on the specified port, default is 6379
12
+ port 9736
13
+
14
+ # If you want you can bind a single interface, if the bind option is not
15
+ # specified all the interfaces will listen for connections.
16
+ #
17
+ # bind 127.0.0.1
18
+
19
+ # Close the connection after a client is idle for N seconds (0 to disable)
20
+ timeout 300
21
+
22
+ # Save the DB on disk:
23
+ #
24
+ # save <seconds> <changes>
25
+ #
26
+ # Will save the DB if both the given number of seconds and the given
27
+ # number of write operations against the DB occurred.
28
+ #
29
+ # In the example below the behaviour will be to save:
30
+ # after 900 sec (15 min) if at least 1 key changed
31
+ # after 300 sec (5 min) if at least 10 keys changed
32
+ # after 60 sec if at least 10000 keys changed
33
+ save 900 1
34
+ save 300 10
35
+ save 60 10000
36
+
37
+ # The filename where to dump the DB
38
+ dbfilename dump.rdb
39
+
40
+ # For default save/load DB in/from the working directory
41
+ # Note that you must specify a directory not a file name.
42
+ dir ./spec/
43
+
44
+ # Set server verbosity to 'debug'
45
+ # it can be one of:
46
+ # debug (a lot of information, useful for development/testing)
47
+ # notice (moderately verbose, what you want in production probably)
48
+ # warning (only very important / critical messages are logged)
49
+ loglevel debug
50
+
51
+ # Specify the log file name. Also 'stdout' can be used to force
52
+ # the demon to log on the standard output. Note that if you use standard
53
+ # output for logging but daemonize, logs will be sent to /dev/null
54
+ logfile stdout
55
+
56
+ # Set the number of databases. The default database is DB 0, you can select
57
+ # a different one on a per-connection basis using SELECT <dbid> where
58
+ # dbid is a number between 0 and 'databases'-1
59
+ databases 16
60
+
61
+ ################################# REPLICATION #################################
62
+
63
+ # Master-Slave replication. Use slaveof to make a Redis instance a copy of
64
+ # another Redis server. Note that the configuration is local to the slave
65
+ # so for example it is possible to configure the slave to save the DB with a
66
+ # different interval, or to listen to another port, and so on.
67
+
68
+ # slaveof <masterip> <masterport>
69
+
70
+ ################################## SECURITY ###################################
71
+
72
+ # Require clients to issue AUTH <PASSWORD> before processing any other
73
+ # commands. This might be useful in environments in which you do not trust
74
+ # others with access to the host running redis-server.
75
+ #
76
+ # This should stay commented out for backward compatibility and because most
77
+ # people do not need auth (e.g. they run their own servers).
78
+
79
+ # requirepass foobared
80
+
81
+ ################################### LIMITS ####################################
82
+
83
+ # Set the max number of connected clients at the same time. By default there
84
+ # is no limit, and it's up to the number of file descriptors the Redis process
85
+ # is able to open. The special value '0' means no limts.
86
+ # Once the limit is reached Redis will close all the new connections sending
87
+ # an error 'max number of clients reached'.
88
+
89
+ # maxclients 128
90
+
91
+ # Don't use more memory than the specified amount of bytes.
92
+ # When the memory limit is reached Redis will try to remove keys with an
93
+ # EXPIRE set. It will try to start freeing keys that are going to expire
94
+ # in little time and preserve keys with a longer time to live.
95
+ # Redis will also try to remove objects from free lists if possible.
96
+ #
97
+ # If all this fails, Redis will start to reply with errors to commands
98
+ # that will use more memory, like SET, LPUSH, and so on, and will continue
99
+ # to reply to most read-only commands like GET.
100
+ #
101
+ # WARNING: maxmemory can be a good idea mainly if you want to use Redis as a
102
+ # 'state' server or cache, not as a real DB. When Redis is used as a real
103
+ # database the memory usage will grow over the weeks, it will be obvious if
104
+ # it is going to use too much memory in the long run, and you'll have the time
105
+ # to upgrade. With maxmemory after the limit is reached you'll start to get
106
+ # errors for write operations, and this may even lead to DB inconsistency.
107
+
108
+ # maxmemory <bytes>
109
+
110
+ ############################### ADVANCED CONFIG ###############################
111
+
112
+ # Glue small output buffers together in order to send small replies in a
113
+ # single TCP packet. Uses a bit more CPU but most of the times it is a win
114
+ # in terms of number of queries per second. Use 'yes' if unsure.
115
+ glueoutputbuf yes
@@ -0,0 +1,76 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), %w[.. lib redis-timeline]))
2
+ dir = File.dirname(File.expand_path(__FILE__))
3
+
4
+ require 'active_record'
5
+ require 'rails'
6
+
7
+ ActiveRecord::Migration.verbose = false
8
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
9
+
10
+
11
+ class Post < ActiveRecord::Base
12
+ include Timeline::Track
13
+ track :new_post, :actor => :user
14
+
15
+ belongs_to :user
16
+ has_many :comments
17
+
18
+ def to_s
19
+ self.title
20
+ end
21
+ end
22
+
23
+ class Comment < ActiveRecord::Base
24
+ include Timeline::Track
25
+
26
+ track :new_comment, :actor => :user, object: [:post_title, :post_id, :body], mentionable: :body, :merge_similar => true
27
+
28
+ belongs_to :post
29
+ belongs_to :user
30
+
31
+ def post_title
32
+ self.post.title if !self.post.blank?
33
+ end
34
+
35
+ def to_s
36
+ self.body
37
+ end
38
+ end
39
+
40
+ class User < ActiveRecord::Base
41
+ include Timeline::Actor
42
+
43
+ has_many :posts
44
+ has_many :comments
45
+
46
+ def to_s
47
+ self.username
48
+ end
49
+ end
50
+
51
+ ActiveRecord::Schema.define(:version => 1) do
52
+ create_table :posts do |t|
53
+ t.column :title, :string
54
+ t.column :body, :text
55
+ t.column :user_id, :integer
56
+ end
57
+
58
+ create_table :users do |t|
59
+ t.column :username, :string
60
+ end
61
+
62
+ create_table :comments do |t|
63
+ t.column :post_id, :integer
64
+ t.column :user_id, :integer
65
+ t.column :body, :text
66
+ end
67
+ end
68
+
69
+ Timeline.redis = '127.0.0.1:6379/tmtest'
70
+
71
+ RSpec.configure do |config|
72
+ config.after :suite do
73
+ keys = Timeline.redis.keys("*")
74
+ Timeline.redis.del(*keys)
75
+ end
76
+ end
@@ -0,0 +1,12 @@
1
+ require File.join(File.dirname(__FILE__), %w[spec_helper])
2
+
3
+ describe Timeline do
4
+ it("can set a redis instance") { Timeline.should respond_to(:redis=) }
5
+ it("has a namespace, timeline") { Timeline.redis.namespace.should == "tmtest" }
6
+
7
+ it "sets the namespace through a url-like string" do
8
+ Timeline.redis = 'localhost:6379/namespace'
9
+ Timeline.redis.namespace.should == 'namespace'
10
+ end
11
+ end
12
+
@@ -0,0 +1,71 @@
1
+ require File.join(File.dirname(__FILE__), %w[spec_helper])
2
+
3
+
4
+
5
+ describe Timeline::Track do
6
+ let(:user) { User.create(username: "first_user") }
7
+ let(:post) { Post.new(user_id: user.id, title: "New post") }
8
+ let(:comment) { Comment.new(user_id: user.id) }
9
+
10
+ describe "included in an ActiveModel-compliant class" do
11
+ it "tracks on create by default" do
12
+ post.should_receive(:track_new_post_after_create)
13
+ post.save
14
+ end
15
+
16
+ it "adds the activity to the global timeline set" do
17
+ post.save
18
+ user.timeline(:global).last.should be_kind_of(Timeline::Activity)
19
+ end
20
+
21
+ it "adds the activity to the actor's timeline" do
22
+ post.save
23
+ user.timeline.last.should be_kind_of(Timeline::Activity)
24
+ end
25
+
26
+ it "cc's the actor's followers by default" do
27
+ follower = User.create(:username => "follower one")
28
+ User.any_instance.should_receive(:followers).and_return([follower])
29
+ post.save
30
+ follower.timeline.last.verb.should == "new_post"
31
+ follower.timeline.last.actor.id.should == user.id
32
+ end
33
+ end
34
+
35
+ describe "with extra_fields" do
36
+ it "stores the extra fields in the timeline" do
37
+ comment.save
38
+ user.timeline.first.object.should respond_to :post_id
39
+ end
40
+ end
41
+
42
+ describe "tracking mentions" do
43
+ it "adds to a user's mentions timeline" do
44
+ User.stub(:find_by_username).and_return(user)
45
+ Comment.create(user_id: user.id, body: "@first_user should see this").save
46
+ user.timeline(:mentions).first.object.body.should == "@first_user should see this"
47
+ end
48
+ end
49
+
50
+ describe "tracking merge similar items" do
51
+ it "should merged" do
52
+ user.timeline(:posts).count.should == 0
53
+ c1 = Comment.create(:user => user, :body => "Comment for merge 1")
54
+ c2 = Comment.create(:user => user, :body => "Comment for merge 2")
55
+ user.timeline(:posts).first.object.class.should == [].class
56
+ user.timeline(:posts).first.object.count.should == 2
57
+ user.timeline(:posts).count.should == 1
58
+ # should not merged affect other user
59
+ user2 = User.create(:username => "user 2")
60
+ Comment.create(:user => user2, :body => "Comment 3")
61
+ user.timeline(:posts).first.object.count.should == 2
62
+ user2.timeline(:posts).first.object.class.should_not == [].class
63
+ # should not merge with added other verbs
64
+ c3 = Comment.create(:user => user, :body => "Comment for merge 3")
65
+ user.timeline(:posts).first.object.count.should == 3
66
+ p1 = Post.create(:user => user, :title => "Post 1")
67
+ c4 = Comment.create(:user => user, :body => "Comment for not merge")
68
+ user.timeline(:posts).first.object.class.should_not == [].class
69
+ end
70
+ end
71
+ end
metadata ADDED
@@ -0,0 +1,164 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: redis-timelineglobal
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.7
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Saurabh Bhatia
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-04-08 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '3.2'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '3.2'
30
+ - !ruby/object:Gem::Dependency
31
+ name: activemodel
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '3.2'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '3.2'
46
+ - !ruby/object:Gem::Dependency
47
+ name: multi_json
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: redis
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: hashie
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :runtime
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: sqlite3
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ description: Redis backed timeline with global activity
111
+ email:
112
+ - saurabh.a.bhatia@gmail.com
113
+ executables: []
114
+ extensions: []
115
+ extra_rdoc_files: []
116
+ files:
117
+ - lib/timeline/track.rb
118
+ - lib/timeline/version.rb
119
+ - lib/timeline/actor.rb
120
+ - lib/timeline/config.rb
121
+ - lib/timeline/activity.rb
122
+ - lib/timeline/helpers.rb
123
+ - lib/redis-timeline.rb
124
+ - lib/tasks/timeline_tasks.rake
125
+ - MIT-LICENSE
126
+ - Rakefile
127
+ - README.md
128
+ - spec/redis-test.conf
129
+ - spec/track_spec.rb
130
+ - spec/activity_spec.rb
131
+ - spec/actor_spec.rb
132
+ - spec/spec_helper.rb
133
+ - spec/timeline_spec.rb
134
+ homepage: http://github.com/saurabhbhatia/redis-timeline
135
+ licenses: []
136
+ post_install_message:
137
+ rdoc_options: []
138
+ require_paths:
139
+ - lib
140
+ required_ruby_version: !ruby/object:Gem::Requirement
141
+ none: false
142
+ requirements:
143
+ - - ! '>='
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ required_rubygems_version: !ruby/object:Gem::Requirement
147
+ none: false
148
+ requirements:
149
+ - - ! '>='
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ requirements: []
153
+ rubyforge_project:
154
+ rubygems_version: 1.8.25
155
+ signing_key:
156
+ specification_version: 3
157
+ summary: Redis backed timeline for your activity feeds.
158
+ test_files:
159
+ - spec/redis-test.conf
160
+ - spec/track_spec.rb
161
+ - spec/activity_spec.rb
162
+ - spec/actor_spec.rb
163
+ - spec/spec_helper.rb
164
+ - spec/timeline_spec.rb