redis-timeline 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.
@@ -0,0 +1,141 @@
1
+ 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
+ followers: :post_participants
37
+
38
+ delegate :participants, to: :post, prefix: true
39
+ end
40
+
41
+ Parameters
42
+ ----------
43
+
44
+ `track` accepts the following parameters...
45
+
46
+ the first param is the verb name.
47
+
48
+ The rest all fit neatly in an options hash.
49
+
50
+ * `on:` [ActiveModel callback]
51
+ You use it to specify whether you want the timeline activity created after a create, update or destroy.
52
+ Default: :create
53
+
54
+ * `actor:` [the method that specifies the object that took this action]
55
+ In the above example, comment.author is this object.
56
+ Default: :creator, so make sure this exists if you don't specify a method here
57
+
58
+ * `object:` defaults to self, which is good most of the time.
59
+ You can override it if you need to
60
+
61
+ * `target:` [related to the `:object` method above. In the example this is the post related to the comment]
62
+ default: nil
63
+
64
+ * `followers:` [who should see this story in their timeline. This references a method on the actor]
65
+ Defaults to the method `followers` defined by Timeline::Actor.
66
+
67
+ * `extra_fields:` [accepts an array of method names that you would like to cache the value of in your timeline]
68
+ Defaults to nil.
69
+
70
+ * `if:` symbol or proc/lambda lets you put conditions on when to track.
71
+
72
+ Display a timeline
73
+ ------------------
74
+
75
+ To retrieve a timeline for a user...
76
+
77
+ class User < ActiveRecord::Base
78
+ include Timeline::Actor
79
+ end
80
+
81
+ The timeline objects are just hashes that are extended by [Hashie](http://github.com/intridea/hashie) to provide method access to the keys.
82
+
83
+ user = User.find(1)
84
+ user.timeline # => [<Timeline::Activity verb='new_comment' ...>]
85
+
86
+ Requirements
87
+ ------------
88
+
89
+ * redis
90
+ * active_support
91
+ * hashie
92
+
93
+ Install
94
+ -------
95
+
96
+ Install redis.
97
+
98
+ Add to your Gemfile:
99
+
100
+ gem 'timeline'
101
+
102
+ Or install it by hand:
103
+
104
+ gem install timeline
105
+
106
+ Setup your redis instance. For a Rails app, something like this...
107
+
108
+ # in config/initializers/redis.rb
109
+
110
+ Timeline.redis = "localhost:6379/timeline"
111
+
112
+ Author
113
+ ------
114
+
115
+ Original author: Felix Clack
116
+
117
+ License
118
+ -------
119
+
120
+ (The MIT License)
121
+
122
+ Copyright (c) 2012 Felix Clack
123
+
124
+ Permission is hereby granted, free of charge, to any person obtaining
125
+ a copy of this software and associated documentation files (the
126
+ 'Software'), to deal in the Software without restriction, including
127
+ without limitation the rights to use, copy, modify, merge, publish,
128
+ distribute, sublicense, and/or sell copies of the Software, and to
129
+ permit persons to whom the Software is furnished to do so, subject to
130
+ the following conditions:
131
+
132
+ The above copyright notice and this permission notice shall be
133
+ included in all copies or substantial portions of the Software.
134
+
135
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
136
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
137
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
138
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
139
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
140
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
141
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -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,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :timeline do
3
+ # # Task goes here
4
+ # end
@@ -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,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,30 @@
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
+ end
27
+ end
28
+ end
29
+ end
30
+ 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,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,92 @@
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
+ @extra_fields = options.delete :extra_fields
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, extra_fields: @extra_fields
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
+ object = !options[:object].nil? ? send(options[:object].to_sym) : self
28
+ target = !options[:target].nil? ? send(options[:target].to_sym) : nil
29
+ followers = actor.send(options[:followers].to_sym)
30
+ add_activity activity(verb: options[:verb], actor: actor, object: object, target: target, extra_fields: options[:extra_fields]), followers
31
+ end
32
+ end
33
+ end
34
+
35
+ protected
36
+ def activity(options={})
37
+ {
38
+ verb: options[:verb],
39
+ actor: options_for(options[:actor]),
40
+ object: options_for(options[:object]),
41
+ target: options_for(options[:target]),
42
+ created_at: Time.now
43
+ }.merge(add_extra_fields(options[:extra_fields]))
44
+ end
45
+
46
+ def add_activity(activity_item, followers)
47
+ redis_add "global:activity", activity_item
48
+ add_activity_to_user(activity_item[:actor][:id], activity_item)
49
+ add_activity_by_user(activity_item[:actor][:id], activity_item)
50
+ add_activity_to_followers(followers, activity_item) if followers.any?
51
+ end
52
+
53
+ def add_activity_to_user(user_id, activity_item)
54
+ redis_add "user:id:#{user_id}:activity", activity_item
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_followers(followers, activity_item)
62
+ followers.each { |follower| add_activity_to_user(follower.id, activity_item) }
63
+ end
64
+
65
+ def add_extra_fields(extra_fields)
66
+ if !extra_fields.nil? and extra_fields.any?
67
+ extras = {}
68
+ extra_fields.each do |value|
69
+ extras[value] = send(value)
70
+ end
71
+ extras
72
+ else
73
+ {}
74
+ end
75
+ end
76
+
77
+ def redis_add(list, activity_item)
78
+ Timeline.redis.lpush list, Timeline.encode(activity_item)
79
+ end
80
+
81
+ def options_for(target)
82
+ if !target.nil?
83
+ {
84
+ id: target.id,
85
+ class: target.class.to_s,
86
+ display_name: target.to_s
87
+ }
88
+ else
89
+ nil
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,3 @@
1
+ module Timeline
2
+ VERSION = "0.1.4"
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) { MultiJson.encode({ 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,23 @@
1
+ require File.join(File.dirname(__FILE__), %w[spec_helper])
2
+
3
+ class User
4
+ include Timeline::Actor
5
+
6
+ attr_accessor :id
7
+ end
8
+
9
+ describe Timeline::Actor do
10
+ describe "when included" do
11
+ before { @user = User.new }
12
+
13
+ it "defines a timeline association" do
14
+ @user.should respond_to :timeline
15
+ end
16
+
17
+ describe ".timeline" do
18
+ subject { @user.timeline }
19
+
20
+ it { should == [] }
21
+ end
22
+ end
23
+ end
Binary file
@@ -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,38 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), %w[.. lib timeline]))
2
+ dir = File.dirname(File.expand_path(__FILE__))
3
+
4
+ RSpec.configure do |config|
5
+
6
+ end
7
+
8
+ #
9
+ # make sure we can run redis
10
+ #
11
+
12
+ if !system("which redis-server")
13
+ puts '', "** can't find `redis-server` in your path"
14
+ puts "** try running `sudo rake install`"
15
+ abort ''
16
+ end
17
+
18
+
19
+ #
20
+ # start our own redis when the tests start,
21
+ # kill it when they end
22
+ #
23
+
24
+ at_exit do
25
+ next if $!
26
+
27
+ exit_code = RSpec::Runner.autorun
28
+
29
+ pid = `ps -A -o pid,command | grep [r]edis-test`.split(" ")[0]
30
+ puts "Killing test redis server..."
31
+ `rm -f #{dir}/dump.rdb`
32
+ Process.kill("KILL", pid.to_i)
33
+ exit exit_code
34
+ end
35
+
36
+ puts "Starting redis for testing at localhost:9736..."
37
+ `redis-server #{dir}/redis-test.conf`
38
+ Timeline.redis = 'localhost:9736'
@@ -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 == :timeline }
6
+
7
+ it "sets the namespace through a url-like string" do
8
+ Timeline.redis = 'localhost:9736/namespace'
9
+ Timeline.redis.namespace.should == 'namespace'
10
+ end
11
+ end
12
+
@@ -0,0 +1,126 @@
1
+ require File.join(File.dirname(__FILE__), %w[spec_helper])
2
+
3
+ require 'active_model'
4
+
5
+ class Post
6
+ extend ActiveModel::Callbacks
7
+
8
+ define_model_callbacks :create
9
+ attr_accessor :id, :to_param, :creator_id, :name
10
+
11
+ include Timeline::Track
12
+ track :new_post
13
+
14
+ def initialize(options={})
15
+ @creator_id = options.delete :creator_id
16
+ @name = options.delete :name
17
+ end
18
+
19
+ def save
20
+ run_callbacks :create
21
+ true
22
+ end
23
+
24
+ def creator
25
+ User.find(creator_id)
26
+ end
27
+
28
+ def to_s
29
+ name
30
+ end
31
+ end
32
+
33
+ class Comment
34
+ extend ActiveModel::Callbacks
35
+
36
+ define_model_callbacks :create
37
+ attr_accessor :id, :creator_id
38
+
39
+ include Timeline::Track
40
+
41
+ track :new_comment, extra_fields: [:post_name, :post_id]
42
+
43
+ def initialize(options={})
44
+ @creator_id = options.delete :creator_id
45
+ end
46
+
47
+ def save
48
+ run_callbacks :create
49
+ true
50
+ end
51
+
52
+ def post_id
53
+ 1
54
+ end
55
+
56
+ def post_name
57
+ "My Post"
58
+ end
59
+
60
+ def creator
61
+ User.find(creator_id)
62
+ end
63
+
64
+ def to_s
65
+ "Comment"
66
+ end
67
+ end
68
+
69
+ class User
70
+ include Timeline::Actor
71
+ attr_accessor :id, :to_param
72
+
73
+ def initialize(options={})
74
+ @id = options.delete :id
75
+ end
76
+
77
+ class << self
78
+ def find user_id
79
+ User.new(id: user_id)
80
+ end
81
+ end
82
+ end
83
+
84
+ describe Timeline::Track do
85
+ let(:creator) { User.new(id: 1) }
86
+ let(:post) { Post.new(creator_id: creator.id, name: "New post") }
87
+
88
+ describe "included in an ActiveModel-compliant class" do
89
+ it "tracks on create by default" do
90
+ post.should_receive(:track_new_post_after_create)
91
+ post.save
92
+ end
93
+
94
+ it "uses the creator as the actor by default" do
95
+ post.should_receive(:creator).and_return(mock("User", id: 1, to_param: "1", followers: []))
96
+ post.save
97
+ end
98
+
99
+ it "adds the activity to the global timeline set" do
100
+ post.save
101
+ creator.timeline(:global).last.should be_kind_of(Timeline::Activity)
102
+ end
103
+
104
+ it "adds the activity to the actor's timeline" do
105
+ post.save
106
+ creator.timeline.last.should be_kind_of(Timeline::Activity)
107
+ end
108
+
109
+ it "cc's the actor's followers by default" do
110
+ follower = User.new(id: 2)
111
+ User.any_instance.should_receive(:followers).and_return([follower])
112
+ post.save
113
+ follower.timeline.last.verb.should == "new_post"
114
+ follower.timeline.last.actor.id.should == 1
115
+ end
116
+ end
117
+
118
+ describe "with extra_fields" do
119
+ let(:comment) { Comment.new(creator_id: creator.id, id: 1) }
120
+
121
+ it "stores the extra fields in the timeline" do
122
+ comment.save
123
+ creator.timeline.first.should respond_to :post_id
124
+ end
125
+ end
126
+ end
metadata ADDED
@@ -0,0 +1,136 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: redis-timeline
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.4
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Felix Clack
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-02-27 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: &70257572121320 !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: *70257572121320
25
+ - !ruby/object:Gem::Dependency
26
+ name: activemodel
27
+ requirement: &70257572120820 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: '3.2'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70257572120820
36
+ - !ruby/object:Gem::Dependency
37
+ name: multi_json
38
+ requirement: &70257572120440 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *70257572120440
47
+ - !ruby/object:Gem::Dependency
48
+ name: redis
49
+ requirement: &70257572119960 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *70257572119960
58
+ - !ruby/object:Gem::Dependency
59
+ name: hashie
60
+ requirement: &70257572119500 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :runtime
67
+ prerelease: false
68
+ version_requirements: *70257572119500
69
+ - !ruby/object:Gem::Dependency
70
+ name: sqlite3
71
+ requirement: &70257572118860 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *70257572118860
80
+ description: ''
81
+ email:
82
+ - felixclack@gmail.com
83
+ executables: []
84
+ extensions: []
85
+ extra_rdoc_files: []
86
+ files:
87
+ - lib/tasks/timeline_tasks.rake
88
+ - lib/timeline/activity.rb
89
+ - lib/timeline/actor.rb
90
+ - lib/timeline/config.rb
91
+ - lib/timeline/helpers.rb
92
+ - lib/timeline/track.rb
93
+ - lib/timeline/version.rb
94
+ - lib/timeline.rb
95
+ - MIT-LICENSE
96
+ - Rakefile
97
+ - README.md
98
+ - spec/activity_spec.rb
99
+ - spec/actor_spec.rb
100
+ - spec/dump.rdb
101
+ - spec/redis-test.conf
102
+ - spec/spec_helper.rb
103
+ - spec/timeline_spec.rb
104
+ - spec/track_spec.rb
105
+ homepage: http://felixclack.github.com/timeline
106
+ licenses: []
107
+ post_install_message:
108
+ rdoc_options: []
109
+ require_paths:
110
+ - lib
111
+ required_ruby_version: !ruby/object:Gem::Requirement
112
+ none: false
113
+ requirements:
114
+ - - ! '>='
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ required_rubygems_version: !ruby/object:Gem::Requirement
118
+ none: false
119
+ requirements:
120
+ - - ! '>='
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ requirements: []
124
+ rubyforge_project:
125
+ rubygems_version: 1.8.11
126
+ signing_key:
127
+ specification_version: 3
128
+ summary: Redis backed timeline for your activity feeds.
129
+ test_files:
130
+ - spec/activity_spec.rb
131
+ - spec/actor_spec.rb
132
+ - spec/dump.rdb
133
+ - spec/redis-test.conf
134
+ - spec/spec_helper.rb
135
+ - spec/timeline_spec.rb
136
+ - spec/track_spec.rb