Skip to content

Commit 02d31ab

Browse files
committed
Add to the timeline
1 parent 5ae619c commit 02d31ab

File tree

10 files changed

+260
-18
lines changed

10 files changed

+260
-18
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
spec/*.rdb

lib/timeline.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1+
require 'active_support'
2+
require 'multi_json'
3+
require 'hashie'
14
require 'timeline/config'
5+
require 'timeline/helpers'
26
require 'timeline/track'
7+
require 'timeline/actor'
8+
require 'timeline/activity'
39

410
module Timeline
5-
extend Timeline::Config
11+
extend Config
12+
extend Helpers
613
end
714

lib/timeline/activity.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module Timeline
2+
class Activity
3+
# include Hashie::Extensions::MethodAccess
4+
5+
def initialize(options={})
6+
Timeline.decode options
7+
end
8+
end
9+
end

lib/timeline/actor.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module Timeline::Actor
2+
extend ActiveSupport::Concern
3+
4+
included do
5+
def timeline(options={})
6+
Timeline.get_list options
7+
end
8+
end
9+
end

lib/timeline/helpers.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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+
defaults = { list_name: "global:activity", start: 0, end: 19 }
21+
if options.is_a? Hash
22+
defaults.merge!(options)
23+
elsif options.is_a? Symbol
24+
case options
25+
when :global
26+
defaults.merge!(list_name: "global:activity")
27+
end
28+
end
29+
Timeline.redis.lrange defaults[:list_name], defaults[:start], defaults[:end]
30+
end
31+
end
32+
end

lib/timeline/track.rb

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
require 'active_support'
2-
31
module Timeline::Track
42
extend ActiveSupport::Concern
53

@@ -9,6 +7,7 @@ def track(name, options={})
97
@callback = options.delete :on
108
@callback ||= :create
119
@actor = options.delete :actor
10+
@subject = options.delete :subject
1211

1312
method_name = "track_#{@name}_after_#{@callback}".to_sym
1413
define_activity_method method_name
@@ -19,26 +18,38 @@ def track(name, options={})
1918
private
2019
def define_activity_method(method_name)
2120
define_method method_name do
22-
actor = @actor || self
23-
add_activity self.activity(@name, actor)
21+
actor = !@actor.nil? ? send(@actor) : creator
22+
subject = !@subject.nil? ? send(@subject.to_sym) : self
23+
add_activity self.activity(@name, actor, subject)
2424
end
2525
end
26-
2726
end
2827

2928
protected
30-
def activity(name, actor)
29+
def activity(name, actor, subject)
3130
{
3231
activity_type: name,
3332
actor: {
3433
actor_id: actor.id,
35-
actor_subject: actor.class.to_s,
36-
url: actor.to_param
34+
actor_class: actor.class.to_s,
35+
url: actor.to_param,
36+
title: actor.to_s
37+
},
38+
subject: {
39+
subject_id: subject.id,
40+
subject_class: subject.class.to_s,
41+
url: subject.to_param,
42+
title: subject.to_s
3743
}
3844
}
3945
end
4046

4147
def add_activity(activity_item)
42-
Timeline.redis.sadd :global, activity_item
48+
redis_add "global:activity", activity_item
49+
redis_add "user:id:#{activity_item[:actor][:actor_id]}:activity", activity_item
50+
end
51+
52+
def redis_add(list, activity_item)
53+
Timeline.redis.lpush list, Timeline.encode(activity_item)
4354
end
4455
end

spec/activity_spec.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
require File.join(File.dirname(__FILE__), %w[spec_helper])
2+
3+
describe Timeline::Activity do
4+
5+
end

spec/actor_spec.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
require File.join(File.dirname(__FILE__), %w[spec_helper])
2+
3+
class User
4+
include Timeline::Actor
5+
end
6+
7+
describe Timeline::Actor do
8+
describe "when included" do
9+
before { @user = User.new }
10+
11+
it "defines a timeline association" do
12+
@user.should respond_to :timeline
13+
end
14+
end
15+
end

spec/redis-test.conf

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

spec/track_spec.rb

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,70 @@
44

55
class Post
66
extend ActiveModel::Callbacks
7-
extend ActiveModel::Serialization
87

98
define_model_callbacks :create
10-
attr_accessor :id, :to_param
9+
attr_accessor :id, :to_param, :creator_id, :name
1110

1211
include Timeline::Track
1312
track :new_post
1413

14+
def initialize(options={})
15+
@creator_id = options.delete :creator_id
16+
@name = options.delete :name
17+
end
18+
1519
def save
1620
run_callbacks :create
1721
true
1822
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 User
34+
include Timeline::Actor
35+
attr_accessor :id, :to_param
36+
37+
def initialize(options={})
38+
@id = options.delete :id
39+
end
40+
41+
class << self
42+
def find user_id
43+
User.new(id: user_id)
44+
end
45+
end
1946
end
2047

2148
describe Timeline::Track do
22-
describe "included in an AR class" do
23-
before { @post = Post.new }
49+
let(:creator) { User.new(id: 1) }
50+
let(:post) { Post.new(creator_id: creator.id, name: "New post") }
2451

52+
describe "included in an AR class" do
2553
it "tracks on create by default" do
26-
@post.should_receive(:track_new_post_after_create)
27-
@post.save
54+
post.should_receive(:track_new_post_after_create)
55+
post.save
56+
end
57+
58+
it "uses the creator as the actor by default" do
59+
post.should_receive(:creator).and_return(mock("User", id: 1, to_param: "1"))
60+
post.save
2861
end
2962

3063
it "adds the activity to the global timeline set" do
31-
Timeline.redis.should_receive(:sadd).with(:global, kind_of(Hash))
32-
@post.save
64+
post.save
65+
creator.timeline(:global).first.should include(post.to_s)
66+
end
67+
68+
it "adds the activity to the actor's timeline" do
69+
post.save
70+
creator.timeline.last.should include(post.to_s)
3371
end
3472
end
3573
end

0 commit comments

Comments
 (0)