├── .rspec
├── .gitignore
├── lib
├── streama
│ ├── version.rb
│ ├── definition_dsl.rb
│ ├── errors.rb
│ ├── definition.rb
│ ├── actor.rb
│ └── activity.rb
└── streama.rb
├── .document
├── spec
├── app
│ └── models
│ │ ├── album.rb
│ │ ├── photo.rb
│ │ ├── no_mongoid.rb
│ │ ├── user.rb
│ │ ├── mars
│ │ └── user.rb
│ │ └── activity.rb
├── lib
│ ├── definition_dsl_spec.rb
│ ├── definition_spec.rb
│ ├── actor_spec.rb
│ └── activity_spec.rb
└── spec_helper.rb
├── Rakefile
├── Gemfile
├── .travis.yml
├── streama.gemspec
├── LICENSE.txt
├── Gemfile.lock
└── README.md
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | .bundle
3 | Gemfile.lock
4 | pkg/*
5 |
--------------------------------------------------------------------------------
/lib/streama/version.rb:
--------------------------------------------------------------------------------
1 | module Streama
2 | VERSION = "0.3.8"
3 | end
4 |
--------------------------------------------------------------------------------
/.document:
--------------------------------------------------------------------------------
1 | lib/**/*.rb
2 | bin/*
3 | -
4 | features/**/*.feature
5 | LICENSE.txt
6 |
--------------------------------------------------------------------------------
/spec/app/models/album.rb:
--------------------------------------------------------------------------------
1 | class Album
2 | include Mongoid::Document
3 |
4 | field :title
5 |
6 | end
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 | require 'rspec/core/rake_task'
3 |
4 | RSpec::Core::RakeTask.new(:spec)
5 |
6 | task default: :spec
7 |
--------------------------------------------------------------------------------
/spec/app/models/photo.rb:
--------------------------------------------------------------------------------
1 | class Photo
2 | include Mongoid::Document
3 | include Mongoid::Attributes::Dynamic
4 |
5 | field :file
6 |
7 | end
8 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 |
3 | # Specify your gem's dependencies in streama.gemspec
4 | gemspec
5 |
6 | gem 'mongoid', '~> 4', github: 'mongoid/mongoid'
7 |
--------------------------------------------------------------------------------
/spec/app/models/no_mongoid.rb:
--------------------------------------------------------------------------------
1 | class NoMongoid
2 | include Streama::Actor
3 |
4 | field :full_name
5 |
6 | def followers
7 | self.class.all
8 | end
9 |
10 | end
--------------------------------------------------------------------------------
/lib/streama.rb:
--------------------------------------------------------------------------------
1 | require "mongoid"
2 | require "streama/version"
3 | require "streama/actor"
4 | require "streama/activity"
5 | require "streama/definition"
6 | require "streama/definition_dsl"
7 | require "streama/errors"
--------------------------------------------------------------------------------
/spec/app/models/user.rb:
--------------------------------------------------------------------------------
1 | class User
2 | include Mongoid::Document
3 | include Streama::Actor
4 |
5 | field :full_name
6 |
7 | def friends
8 | self.class.all
9 | end
10 |
11 | def followers
12 | self.class.all
13 | end
14 |
15 | end
--------------------------------------------------------------------------------
/spec/app/models/mars/user.rb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | module Mars
3 | class User
4 | include Mongoid::Document
5 | include Streama::Actor
6 |
7 | field :full_name
8 |
9 | def followers
10 | self.class.all
11 | end
12 |
13 | end
14 |
15 | end
16 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | services: mongodb
3 | matrix:
4 | include:
5 | - rvm: 1.9.3
6 | - rvm: ruby-head
7 | - rvm: jruby-19mode
8 | jdk: oraclejdk7
9 | env: JRUBY_OPTS="-Xmx512m"
10 | - rvm: jruby-19mode
11 | jdk: openjdk7
12 | env: JRUBY_OPTS="-Xmx512m"
13 | - rvm: jruby-head
14 | jdk: oraclejdk7
15 | env: JRUBY_OPTS="-Xmx512m"
16 | - rvm: jruby-head
17 | jdk: openjdk7
18 | env: JRUBY_OPTS="-Xmx512m"
--------------------------------------------------------------------------------
/spec/app/models/activity.rb:
--------------------------------------------------------------------------------
1 | class Activity
2 | include Streama::Activity
3 |
4 | activity :new_photo do
5 | actor :user, :cache => [:full_name]
6 | object :photo, :cache => [:file]
7 | target_object :album, :cache => [:title]
8 | end
9 |
10 | activity :new_photo_without_cache do
11 | actor :user
12 | object :photo
13 | target_object :album
14 | end
15 |
16 | activity :new_comment do
17 | actor :user, :cache => [:full_name]
18 | object :photo
19 | end
20 |
21 | activity :new_tag do
22 | actor :user, :cache => [:full_name]
23 | object :photo
24 | end
25 |
26 | activity :new_mars_photo do
27 | actor :user, :cache => [:full_name], :class_name => 'Mars::User'
28 | object :photo
29 | target_object :album, :cache => [:title]
30 | end
31 |
32 | end
33 |
--------------------------------------------------------------------------------
/lib/streama/definition_dsl.rb:
--------------------------------------------------------------------------------
1 | module Streama
2 |
3 | class DefinitionDSL
4 |
5 | attr_reader :attributes
6 |
7 | def initialize(name)
8 | @attributes = {
9 | :name => name.to_sym,
10 | :actor => {},
11 | :object => {},
12 | :target_object => {}
13 | }
14 | end
15 |
16 | delegate :[], :to => :@attributes
17 |
18 | def self.data_methods(*args)
19 | args.each do |method|
20 | define_method method do |*args|
21 | class_sym = if class_name = args[1].try(:delete,:class_name)
22 | class_name.underscore.to_sym
23 | else
24 | args[0].is_a?(Symbol) ? args[0] : args[0].class.to_sym
25 | end
26 | @attributes[method].store(class_sym, args[1])
27 | end
28 | end
29 | end
30 | data_methods :actor, :object, :target_object
31 |
32 | end
33 |
34 | end
35 |
--------------------------------------------------------------------------------
/streama.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | $:.push File.expand_path("../lib", __FILE__)
3 | require "streama/version"
4 |
5 | Gem::Specification.new do |s|
6 | s.name = "streama"
7 | s.version = Streama::VERSION
8 | s.authors = ["Christos Pappas"]
9 | s.email = ["christos.pappas@gmail.com"]
10 | s.homepage = ""
11 | s.summary = %q{Activity Streams for Mongoid}
12 | s.description = %q{Streama is a simple activity stream gem for use with the Mongoid ODM framework}
13 |
14 | s.rubyforge_project = "streama"
15 |
16 | s.files = `git ls-files`.split("\n")
17 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19 | s.require_paths = ["lib"]
20 |
21 | s.add_dependency("mongoid", ">= 3.0", "<= 5")
22 |
23 | s.add_development_dependency "rspec", "~> 2.5"
24 | s.add_development_dependency "database_cleaner", "~> 0.8"
25 | s.add_development_dependency "pry"
26 | s.add_development_dependency "rake"
27 | end
28 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2011 Christos Pappas
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.
21 |
--------------------------------------------------------------------------------
/lib/streama/errors.rb:
--------------------------------------------------------------------------------
1 | module Streama
2 |
3 | module Errors
4 |
5 | class StreamaError < StandardError
6 | end
7 |
8 | class InvalidActivity < StreamaError
9 | end
10 |
11 | # This error is raised when an object isn't defined
12 | # as an actor, object or target
13 | #
14 | # Example:
15 | #
16 | # InvalidField.new('field_name')
17 | class InvalidData < StreamaError
18 | attr_reader :message
19 |
20 | def initialize message
21 | @message = "Invalid Data: #{message}"
22 | end
23 |
24 | end
25 |
26 | # This error is raised when trying to store a field that doesn't exist
27 | #
28 | # Example:
29 | #
30 | # InvalidField.new('field_name')
31 | class InvalidField < StreamaError
32 | attr_reader :message
33 |
34 | def initialize message
35 | @message = "Invalid Field: #{message}"
36 | end
37 |
38 | end
39 |
40 | class ActivityNotSaved < StreamaError
41 | end
42 |
43 | class NoFollowersDefined < StreamaError
44 | end
45 |
46 | class NotMongoid < StreamaError
47 | end
48 |
49 | end
50 |
51 | end
--------------------------------------------------------------------------------
/spec/lib/definition_dsl_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "Definition" do
4 |
5 | let(:definition_dsl) {Streama::DefinitionDSL.new(:new_enquiry)}
6 |
7 | it "initializes with name" do
8 | definition_dsl.attributes[:name].should eq :new_enquiry
9 | end
10 |
11 | it "adds an actor to the definition" do
12 | dsl = definition_dsl
13 | dsl.actor(:user, :cache => [:id, :full_name])
14 | dsl.attributes[:actor].should eq :user => { :cache=>[:id, :full_name] }
15 | end
16 |
17 | it "adds multiple actors to the definition" do
18 | dsl = definition_dsl
19 | dsl.actor(:user, :cache => [:id, :full_name])
20 | dsl.actor(:company, :cache => [:id, :name])
21 | dsl.attributes[:actor].should eq :user => { :cache=>[:id, :full_name] }, :company => { :cache=>[:id, :name] }
22 | end
23 |
24 | it "adds an object to the definition" do
25 | dsl = definition_dsl
26 | dsl.object(:listing, :cache => [:id, :title])
27 | dsl.attributes[:object].should eq :listing => { :cache=>[:id, :title] }
28 | end
29 |
30 | it "adds a target to the definition" do
31 | dsl = definition_dsl
32 | dsl.target_object(:company, :cache => [:id, :name])
33 | dsl.attributes[:target_object].should eq :company => { :cache=>[:id, :name] }
34 | end
35 |
36 | end
37 |
--------------------------------------------------------------------------------
/lib/streama/definition.rb:
--------------------------------------------------------------------------------
1 | module Streama
2 |
3 | class Definition
4 |
5 | attr_reader :name, :actor, :object, :target_object, :receivers
6 |
7 | # @param dsl [Streama::DefinitionDSL] A DSL object
8 | def initialize(definition)
9 | @name = definition[:name]
10 | @actor = definition[:actor] || {}
11 | @object = definition[:object] || {}
12 | @target_object = definition[:target_object] || {}
13 | end
14 |
15 | #
16 | # Registers a new definition
17 | #
18 | # @param definition [Definition] The definition to register
19 | # @return [Definition] Returns the registered definition
20 | def self.register(definition)
21 | return false unless definition.is_a? DefinitionDSL
22 | definition = new(definition)
23 | self.registered << definition
24 | return definition || false
25 | end
26 |
27 | # List of registered definitions
28 | # @return [Array]
29 | def self.registered
30 | @definitions ||= []
31 | end
32 |
33 | def self.find(name)
34 | unless definition = registered.find{|definition| definition.name == name.to_sym}
35 | raise Streama::Errors::InvalidActivity, "Could not find a definition for `#{name}`"
36 | else
37 | definition
38 | end
39 | end
40 |
41 | end
42 |
43 | end
--------------------------------------------------------------------------------
/lib/streama/actor.rb:
--------------------------------------------------------------------------------
1 | module Streama
2 |
3 | module Actor
4 | extend ActiveSupport::Concern
5 |
6 | included do
7 | raise Errors::NotMongoid, "Must be included in a Mongoid::Document" unless self.ancestors.include? Mongoid::Document
8 |
9 | cattr_accessor :activity_klass
10 | end
11 |
12 | module ClassMethods
13 |
14 | def activity_class(klass)
15 | self.activity_klass = klass.to_s
16 | end
17 |
18 | end
19 |
20 | # Publishes the activity to the receivers
21 | #
22 | # @param [ Hash ] options The options to publish with.
23 | #
24 | # @example publish an activity with a object and target
25 | # current_user.publish_activity(:enquiry, :object => @enquiry, :target => @listing)
26 | #
27 | def publish_activity(name, options={})
28 | options[:receivers] = self.send(options[:receivers]) if options[:receivers].is_a?(Symbol)
29 | activity = activity_class.publish(name, {:actor => self}.merge(options))
30 | end
31 |
32 | def activity_stream(options = {})
33 | activity_class.stream_for(self, options)
34 | end
35 |
36 | def published_activities(options = {})
37 | activity_class.stream_of(self, options)
38 | end
39 |
40 | def activity_class
41 | @activity_klass ||= activity_klass ? activity_klass.classify.constantize : ::Activity
42 | end
43 | end
44 |
45 | end
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require "pry"
2 | $LOAD_PATH.unshift(File.dirname(__FILE__))
3 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
4 |
5 | MODELS = File.join(File.dirname(__FILE__), "app/models")
6 | SUPPORT = File.join(File.dirname(__FILE__), "support")
7 | $LOAD_PATH.unshift(MODELS)
8 | $LOAD_PATH.unshift(SUPPORT)
9 |
10 | require 'streama'
11 | require 'mongoid'
12 | require 'rspec'
13 | require 'database_cleaner'
14 |
15 | LOGGER = Logger.new($stdout)
16 |
17 | DatabaseCleaner.strategy = :truncation
18 |
19 | def database_id
20 | ENV["CI"] ? "mongoid_#{Process.pid}" : "mongoid_test"
21 | end
22 |
23 | Mongoid.configure do |config|
24 | config.connect_to(database_id)
25 | end
26 |
27 | Dir[ File.join(MODELS, "*.rb") ].sort.each do |file|
28 | name = File.basename(file, ".rb")
29 | autoload name.camelize.to_sym, name
30 | end
31 | require File.join(MODELS,"mars","user.rb")
32 |
33 | Dir[ File.join(SUPPORT, "*.rb") ].each do |file|
34 | require File.basename(file)
35 | end
36 |
37 | RSpec.configure do |config|
38 | config.include RSpec::Matchers
39 | config.mock_with :rspec
40 |
41 | config.before(:each) do
42 | DatabaseCleaner.start
43 | Mongoid::IdentityMap.clear
44 | end
45 |
46 | config.after(:each) do
47 | DatabaseCleaner.clean
48 | end
49 |
50 | config.after(:suite) do
51 | if ENV["CI"]
52 | Mongoid::Threaded.sessions[:default].drop
53 | end
54 | end
55 |
56 | end
57 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GIT
2 | remote: git://github.com/mongoid/mongoid.git
3 | revision: 9b3bc1264032209b7a6c0e82d0ca656f401e476b
4 | specs:
5 | mongoid (4.0.0)
6 | activemodel (~> 4.0.0)
7 | moped (~> 2.0.beta3)
8 | origin (~> 1.0)
9 | tzinfo (~> 0.3.37)
10 |
11 | PATH
12 | remote: .
13 | specs:
14 | streama (0.3.8)
15 | mongoid (>= 3.0, <= 5)
16 |
17 | GEM
18 | remote: http://rubygems.org/
19 | specs:
20 | activemodel (4.0.0)
21 | activesupport (= 4.0.0)
22 | builder (~> 3.1.0)
23 | activesupport (4.0.0)
24 | i18n (~> 0.6, >= 0.6.4)
25 | minitest (~> 4.2)
26 | multi_json (~> 1.3)
27 | thread_safe (~> 0.1)
28 | tzinfo (~> 0.3.37)
29 | atomic (1.1.14)
30 | bson (3.2.7)
31 | builder (3.1.4)
32 | coderay (1.0.8)
33 | concurrent-ruby (1.1.6)
34 | connection_pool (2.2.2)
35 | database_cleaner (0.8.0)
36 | diff-lcs (1.1.3)
37 | i18n (0.9.5)
38 | concurrent-ruby (~> 1.0)
39 | method_source (0.8.1)
40 | minitest (4.7.5)
41 | moped (2.0.7)
42 | bson (~> 3.0)
43 | connection_pool (~> 2.0)
44 | optionable (~> 0.2.0)
45 | multi_json (1.8.2)
46 | optionable (0.2.0)
47 | origin (1.1.0)
48 | pry (0.9.10)
49 | coderay (~> 1.0.5)
50 | method_source (~> 0.8)
51 | slop (~> 3.3.1)
52 | rake (13.0.1)
53 | rspec (2.6.0)
54 | rspec-core (~> 2.6.0)
55 | rspec-expectations (~> 2.6.0)
56 | rspec-mocks (~> 2.6.0)
57 | rspec-core (2.6.4)
58 | rspec-expectations (2.6.0)
59 | diff-lcs (~> 1.1.2)
60 | rspec-mocks (2.6.0)
61 | slop (3.3.3)
62 | thread_safe (0.1.3)
63 | atomic
64 | tzinfo (0.3.38)
65 |
66 | PLATFORMS
67 | ruby
68 |
69 | DEPENDENCIES
70 | database_cleaner (~> 0.8)
71 | mongoid (~> 4)!
72 | pry
73 | rake
74 | rspec (~> 2.5)
75 | streama!
76 |
--------------------------------------------------------------------------------
/spec/lib/definition_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "Definition" do
4 |
5 | let(:definition_dsl) do
6 | dsl = Streama::DefinitionDSL.new(:new_photo)
7 | dsl.actor(:user, :cache => [:id, :full_name])
8 | dsl.object(:photo, :cache => [:id, :full_name])
9 | dsl.target_object(:album, :cache => [:id, :name, :full_address])
10 | dsl
11 | end
12 |
13 | describe '#initialize' do
14 | before(:all) do
15 | @definition_dsl = definition_dsl
16 | @definition = Streama::Definition.new(@definition_dsl)
17 | end
18 |
19 | it "assigns @actor" do
20 | @definition.actor.has_key?(:user).should be true
21 | end
22 | it "assigns @object" do
23 | @definition.object.has_key?(:photo).should be true
24 | end
25 |
26 | it "assigns @target" do
27 | @definition.target_object.has_key?(:album).should be true
28 | end
29 |
30 | end
31 |
32 | describe '.register' do
33 |
34 | it "registers a definition and return new definition" do
35 | Streama::Definition.register(definition_dsl).is_a?(Streama::Definition).should eq true
36 | end
37 |
38 | it "returns false if invalid definition" do
39 | Streama::Definition.register(false).should be false
40 | end
41 |
42 | end
43 |
44 | describe '.registered' do
45 |
46 | it "returns registered definitions" do
47 | Streama::Definition.register(definition_dsl)
48 | Streama::Definition.registered.size.should be > 0
49 | end
50 |
51 | end
52 |
53 | describe '.find' do
54 |
55 | it "returns the definition by name" do
56 | Streama::Definition.find(:new_photo).name.should eq :new_photo
57 | end
58 |
59 | it "raises an exception if invalid activity" do
60 | lambda { Streama::Definition.find(:unknown_activity) }.should raise_error Streama::Errors::InvalidActivity
61 | end
62 |
63 | end
64 |
65 | end
66 |
--------------------------------------------------------------------------------
/spec/lib/actor_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "Actor" do
4 |
5 | let(:photo) { Photo.create(:comment => "I'm interested") }
6 | let(:album) { Album.create(:title => "A test album") }
7 | let(:user) { User.create(:full_name => "Christos") }
8 |
9 | it "raises an exception if the class is not a mongoid document" do
10 | lambda { NoMongoid.new }.should raise_error Streama::Errors::NotMongoid
11 | end
12 |
13 | describe "#publish_activity" do
14 | before :each do
15 | 2.times { |n| User.create(:full_name => "Receiver #{n}") }
16 | end
17 |
18 | it "pushes activity to receivers" do
19 | activity = user.publish_activity(:new_photo, :object => photo, :target_object => album)
20 | activity.receivers.size == 6
21 | end
22 |
23 | it "pushes to a defined stream" do
24 | activity = user.publish_activity(:new_photo, :object => photo, :target_object => album, :receivers => :friends)
25 | activity.receivers.size == 6
26 | end
27 |
28 | end
29 |
30 | describe "#activity_stream" do
31 |
32 | before :each do
33 | user.publish_activity(:new_photo, :object => photo, :target_object => album)
34 | user.publish_activity(:new_comment, :object => photo)
35 |
36 | u = User.create(:full_name => "Other User")
37 | u.publish_activity(:new_photo, :object => photo, :target_object => album)
38 | u.publish_activity(:new_tag, :object => photo)
39 |
40 | end
41 |
42 | it "retrieves the stream for an actor" do
43 | user.activity_stream.size.should eq 4
44 | end
45 |
46 | it "retrieves the stream and filters to a particular activity type" do
47 | user.activity_stream(:type => :new_photo).size.should eq 2
48 | end
49 |
50 | it "retrieves the stream and filters to a couple particular activity types" do
51 | user.activity_stream(:type => [:new_tag, :new_comment]).size.should eq 2
52 | end
53 |
54 | end
55 |
56 | describe "#published_activities" do
57 | before :each do
58 | user.publish_activity(:new_photo, :object => photo, :target_object => album)
59 | user.publish_activity(:new_comment, :object => photo)
60 | user.publish_activity(:new_tag, :object => photo)
61 |
62 | u = User.create(:full_name => "Other User")
63 | u.publish_activity(:new_photo, :object => photo, :target_object => album)
64 | end
65 |
66 | it "retrieves published activities for the actor" do
67 | user.published_activities.size.should eq 3
68 | end
69 |
70 | it "retrieves and filters published activities by type for the actor" do
71 | user.published_activities(:type => :new_photo).size.should eq 1
72 | end
73 |
74 | it "retrieves and filters published activities by a couple types for the actor" do
75 | user.published_activities(:type => [:new_comment, :new_tag]).size.should eq 2
76 | end
77 |
78 | end
79 |
80 |
81 | end
82 |
--------------------------------------------------------------------------------
/spec/lib/activity_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "Activity" do
4 |
5 | let(:photo) { Photo.create(:file => "image.jpg") }
6 | let(:album) { Album.create(:title => "A test album") }
7 | let(:user) { User.create(:full_name => "Christos") }
8 | let(:mars_user) { Mars::User.create(:full_name => "Mars User") }
9 |
10 | describe ".activity" do
11 | it "registers and return a valid definition" do
12 | @definition = Activity.activity(:test_activity) do
13 | actor :user, :cache => [:full_name]
14 | object :photo, :cache => [:file]
15 | target_object :album, :cache => [:title]
16 | end
17 |
18 | @definition.is_a?(Streama::Definition).should be true
19 | end
20 |
21 | end
22 |
23 | describe "#publish" do
24 |
25 | before :each do
26 | @send_to = []
27 | 2.times { |n| @send_to << User.create(:full_name => "Custom Receiver #{n}") }
28 | 5.times { |n| User.create(:full_name => "Receiver #{n}") }
29 | end
30 |
31 | it "pushes activity to receivers" do
32 | @activity = Activity.publish(:new_photo, {:actor => user, :object => photo, :target_object => album, :receivers => @send_to})
33 | @activity.receivers.size.should == 2
34 | end
35 |
36 |
37 | context "when activity not cached" do
38 |
39 | it "pushes activity to receivers" do
40 | @activity = Activity.publish(:new_photo_without_cache, {:actor => user, :object => photo, :target_object => album, :receivers => @send_to})
41 | @activity.receivers.size.should == 2
42 | end
43 |
44 | end
45 |
46 | it "overrides the recievers if option passed" do
47 | @activity = Activity.publish(:new_photo, {:actor => user, :object => photo, :target_object => album, :receivers => @send_to})
48 | @activity.receivers.size.should == 2
49 | end
50 |
51 |
52 |
53 | context "when republishing"
54 | before :each do
55 | @actor = user
56 | @activity = Activity.publish(:new_photo, {:actor => @actor, :object => photo, :target_object => album})
57 | @activity.publish
58 | end
59 |
60 | it "updates metadata" do
61 | @actor.full_name = "testing"
62 | @actor.save
63 | @activity.publish
64 | @activity.actor['full_name'].should eq "testing"
65 | end
66 | end
67 |
68 | describe ".publish" do
69 | it "creates a new activity" do
70 | activity = Activity.publish(:new_photo, {:actor => user, :object => photo, :target_object => album})
71 | activity.should be_an_instance_of Activity
72 | end
73 |
74 | it " creates a new activity when actor has namespace" do
75 | activity = Activity.publish(:new_mars_photo, {:actor => mars_user, :object => photo, :target_object => album})
76 | activity.should be_an_instance_of Activity
77 | end
78 |
79 | end
80 |
81 | describe "#refresh" do
82 |
83 | before :each do
84 | @user = user
85 | @activity = Activity.publish(:new_photo, {:actor => @user, :object => photo, :target_object => album})
86 | end
87 |
88 | it "reloads instances and updates activities stored data" do
89 | @activity.save
90 | @activity = Activity.last
91 |
92 | expect do
93 | @user.update_attribute(:full_name, "Test")
94 | @activity.refresh_data
95 | end.to change{ @activity.load_instance(:actor).full_name}.from("Christos").to("Test")
96 | end
97 |
98 | end
99 |
100 | describe "#load_instance" do
101 |
102 | before :each do
103 | @activity = Activity.publish(:new_photo, {:actor => user, :object => photo, :target_object => album})
104 | @activity = Activity.last
105 | end
106 |
107 | it "loads an actor instance" do
108 | @activity.load_instance(:actor).should be_instance_of User
109 | end
110 |
111 | it "loads an object instance" do
112 | @activity.load_instance(:object).should be_instance_of Photo
113 | end
114 |
115 | it "loads a target instance" do
116 | @activity.load_instance(:target_object).should be_instance_of Album
117 | end
118 |
119 | end
120 |
121 | end
122 |
--------------------------------------------------------------------------------
/lib/streama/activity.rb:
--------------------------------------------------------------------------------
1 | module Streama
2 | module Activity
3 | extend ActiveSupport::Concern
4 |
5 | included do
6 |
7 | include Mongoid::Document
8 | include Mongoid::Timestamps
9 |
10 | field :verb, :type => Symbol
11 | field :actor
12 | field :object
13 | field :target_object
14 | field :receivers, :type => Array
15 |
16 | index({ 'actor._id' => 1, 'actor._type' => 1 })
17 | index({ 'object._id' => 1, 'object._type' => 1 })
18 | index({ 'target_object._id' => 1, 'target_object._type' => 1 })
19 | index({ 'receivers.id' => 1, 'receivers.type' => 1 })
20 |
21 | validates_presence_of :actor, :verb
22 | before_save :assign_data
23 |
24 | end
25 |
26 | module ClassMethods
27 |
28 | # Defines a new activity type and registers a definition
29 | #
30 | # @param [ String ] name The name of the activity
31 | #
32 | # @example Define a new activity
33 | # activity(:enquiry) do
34 | # actor :user, :cache => [:full_name]
35 | # object :enquiry, :cache => [:subject]
36 | # target_object :listing, :cache => [:title]
37 | # end
38 | #
39 | # @return [Definition] Returns the registered definition
40 | def activity(name, &block)
41 | definition = Streama::DefinitionDSL.new(name)
42 | definition.instance_eval(&block)
43 | Streama::Definition.register(definition)
44 | end
45 |
46 | # Publishes an activity using an activity name and data
47 | #
48 | # @param [ String ] verb The verb of the activity
49 | # @param [ Hash ] data The data to initialize the activity with.
50 | #
51 | # @return [Streama::Activity] An Activity instance with data
52 | def publish(verb, data)
53 | receivers = data.delete(:receivers)
54 | new({:verb => verb}.merge(data)).publish(:receivers => receivers)
55 | end
56 |
57 | def stream_for(actor, options={})
58 | query = {:receivers => {'$elemMatch' => {:id => actor.id, :type => actor.class.to_s}}}
59 | query.merge!({:verb.in => [*options[:type]]}) if options[:type]
60 | self.where(query).desc(:created_at)
61 | end
62 |
63 | def stream_of(actor, options={})
64 | query = {'actor.id' => actor.id, 'actor.type' => actor.class.to_s}
65 | query.merge!({:verb.in => [*options[:type]]}) if options[:type]
66 | self.where(query).desc(:created_at)
67 | end
68 |
69 | end
70 |
71 |
72 | # Publishes the activity to the receivers
73 | #
74 | # @param [ Hash ] options The options to publish with.
75 | #
76 | def publish(options = {})
77 | actor = load_instance(:actor)
78 | self.receivers = (options[:receivers] || actor.followers).map { |r| { :id => r.id, :type => r.class.to_s } }
79 | self.save
80 | self
81 | end
82 |
83 | # Returns an instance of an actor, object or target
84 | #
85 | # @param [ Symbol ] type The data type (actor, object, target) to return an instance for.
86 | #
87 | # @return [Mongoid::Document] document A mongoid document instance
88 | def load_instance(type)
89 | (data = self.read_attribute(type)).is_a?(Hash) ? data['type'].to_s.camelcase.constantize.find(data['id']) : data
90 | end
91 |
92 | def refresh_data
93 | assign_data
94 | save(:validates_presence_of => false)
95 | end
96 |
97 | protected
98 |
99 | def assign_data
100 |
101 | [:actor, :object, :target_object].each do |type|
102 | next unless object = load_instance(type)
103 |
104 | class_sym = object.class.name.underscore.to_sym
105 |
106 | raise Errors::InvalidData.new(class_sym) unless definition.send(type).has_key?(class_sym)
107 |
108 | hash = {'id' => object.id, 'type' => object.class.name}
109 |
110 | if fields = definition.send(type)[class_sym].try(:[],:cache)
111 | fields.each do |field|
112 | raise Errors::InvalidField.new(field) unless object.respond_to?(field)
113 | hash[field.to_s] = object.send(field)
114 | end
115 | end
116 | write_attribute(type, hash)
117 | end
118 | end
119 |
120 | def definition
121 | @definition ||= Streama::Definition.find(verb)
122 | end
123 |
124 | end
125 | end
126 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Streama
2 |
3 | * THIS PROJECT IS NO LONGER MAINTAINED *
4 |
5 | Streama is a simple Ruby activity stream gem for use with the Mongoid ODM framework.
6 |
7 | It works by posting to and querying from a firehose of individual activity items.
8 |
9 | **Currently Streama uses a Fan Out On Read approach. This is great for single instance databases, however if you plan on Sharding then please be aware that it'll hit every shard when querying. I plan on changing the schema soon so that it Fans Out On Write with bucketing.**
10 |
11 | [Data Modeling Examples from the real world](http://www.10gen.com/presentations/data-modeling-examples-real-world)
12 |
13 | [](http://travis-ci.org/christospappas/streama)
14 | [](https://codeclimate.com/github/christospappas/streama)
15 |
16 | ## Project Tracking
17 |
18 | * [Streama Google Group](http://groups.google.com/group/streama)
19 | * [Code Climate](https://codeclimate.com/github/christospappas/streama)
20 | * [Website Demo](http://streamaweb.info)
21 |
22 | ## Install
23 |
24 | gem install streama
25 |
26 | ## Usage
27 |
28 | ### Define Activities
29 |
30 | Create an Activity model and define the activities and the fields you would like to cache within the activity.
31 |
32 | An activity consists of an actor, a verb, an object, and a target.
33 |
34 | ``` ruby
35 | class Activity
36 | include Streama::Activity
37 |
38 | activity :new_photo do
39 | actor :user, :cache => [:full_name]
40 | object :photo, :cache => [:subject, :comment]
41 | target_object :album, :cache => [:title]
42 | end
43 |
44 | end
45 | ```
46 |
47 | The activity verb is implied from the activity name, in the above example the verb is :new_photo
48 |
49 | The object may be the entity performing the activity, or the entity on which the activity was performed.
50 | e.g John(actor) shared a video(object)
51 |
52 | The target is the object that the verb is enacted on.
53 | e.g. Geraldine(actor) posted a photo(object) to her album(target)
54 |
55 | This is based on the Activity Streams 1.0 specification (http://activitystrea.ms)
56 |
57 | ### Setup Actors
58 |
59 | Include the Actor module in a class and override the default followers method.
60 |
61 | ``` ruby
62 | class User
63 | include Mongoid::Document
64 | include Streama::Actor
65 |
66 | field :full_name, :type => String
67 |
68 | def followers
69 | User.excludes(:id => self.id).all
70 | end
71 | end
72 | ```
73 |
74 | ### Setup Indexes
75 |
76 | Create the indexes for the Activities collection. You can do so by calling the create_indexes method.
77 |
78 | ``` ruby
79 | Activity.create_indexes
80 | ```
81 |
82 | ### Publishing Activity
83 |
84 | In your controller or background worker:
85 |
86 | ``` ruby
87 | current_user.publish_activity(:new_photo, :object => @photo, :target_object => @album)
88 | ```
89 |
90 | This will publish the activity to the mongoid objects returned by the #followers method in the Actor.
91 |
92 | To send your activity to different receievers, pass in an additional :receivers parameter.
93 |
94 | ``` ruby
95 | current_user.publish_activity(:new_photo, :object => @photo, :target_object => @album, :receivers => :friends) # calls friends method
96 | ```
97 |
98 | ``` ruby
99 | current_user.publish_activity(:new_photo, :object => @photo, :target_object => @album, :receivers => current_user.find(:all, :conditions => {:group_id => mygroup}))
100 | ```
101 |
102 | ## Retrieving Activity
103 |
104 | To retrieve the activity stream for an actor
105 |
106 | ``` ruby
107 | current_user.activity_stream
108 | ```
109 |
110 | To retrieve the activity stream and filter by activity type
111 |
112 | ``` ruby
113 | current_user.activity_stream(:type => :activity_verb)
114 | ```
115 |
116 | To retrieve all activities published by an actor
117 |
118 | ``` ruby
119 | current_user.published_activities
120 | ```
121 |
122 | To retrieve all activities published by an actor and filtered by activity type
123 |
124 | ``` ruby
125 | current_user.published_activities(:type => :activity_verb)
126 | ```
127 |
128 | If you need to return the instance of an :actor, :object or :target_object from an activity call the Activity#load_instance method
129 |
130 | ``` ruby
131 | activity.load_instance(:actor)
132 | ```
133 |
134 | You can also refresh the cached activity data by calling the Activity#refresh_data method
135 |
136 | ``` ruby
137 | activity.refresh_data
138 | ```
139 |
140 | ## Upgrading
141 |
142 | ### 0.3.8
143 |
144 | Mongoid 4 support added.
145 |
146 | ### 0.3.6
147 |
148 | Mongoid 3.0 support added.
149 |
150 | ### 0.3.3
151 |
152 | The Activity "target" field was renamed to "target_object". If you are upgrading from a previous version of Streama you will need to rename the field in existing documents.
153 |
154 | http://www.mongodb.org/display/DOCS/Updating#Updating-%24rename
155 |
156 | ## Contributing
157 |
158 | Once you've made your great commits
159 |
160 | 1. Fork
161 | 1. Create a topic branch - git checkout -b my_branch
162 | 1. Push to your branch - git push origin my_branch
163 | 1. Create a Pull Request from your branch
164 | 1. That's it!
165 |
--------------------------------------------------------------------------------