timeful 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +112 -0
  4. data/Rakefile +31 -0
  5. data/app/jobs/timeful/deliver_activity_to_subscribers_job.rb +15 -0
  6. data/app/models/timeful/activity.rb +34 -0
  7. data/app/models/timeful/application_record.rb +6 -0
  8. data/app/models/timeful/feed_item.rb +13 -0
  9. data/config/routes.rb +2 -0
  10. data/db/migrate/20160927141608_create_timeful_activities.rb +16 -0
  11. data/db/migrate/20160927154912_create_timeful_feed_items.rb +15 -0
  12. data/lib/generators/timeful/install_generator.rb +10 -0
  13. data/lib/timeful.rb +28 -0
  14. data/lib/timeful/engine.rb +6 -0
  15. data/lib/timeful/model/actor.rb +35 -0
  16. data/lib/timeful/model/subscriber.rb +21 -0
  17. data/lib/timeful/relation_proxy.rb +26 -0
  18. data/lib/timeful/version.rb +4 -0
  19. data/spec/dummy/README.rdoc +28 -0
  20. data/spec/dummy/Rakefile +6 -0
  21. data/spec/dummy/app/activities/post_activity.rb +5 -0
  22. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  23. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  24. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  25. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  26. data/spec/dummy/app/models/application_record.rb +3 -0
  27. data/spec/dummy/app/models/post.rb +2 -0
  28. data/spec/dummy/app/models/user.rb +4 -0
  29. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  30. data/spec/dummy/bin/bundle +3 -0
  31. data/spec/dummy/bin/rails +4 -0
  32. data/spec/dummy/bin/rake +4 -0
  33. data/spec/dummy/bin/setup +29 -0
  34. data/spec/dummy/config.ru +4 -0
  35. data/spec/dummy/config/application.rb +22 -0
  36. data/spec/dummy/config/boot.rb +5 -0
  37. data/spec/dummy/config/database.example.yml +19 -0
  38. data/spec/dummy/config/database.yml +19 -0
  39. data/spec/dummy/config/environment.rb +5 -0
  40. data/spec/dummy/config/environments/development.rb +41 -0
  41. data/spec/dummy/config/environments/production.rb +79 -0
  42. data/spec/dummy/config/environments/test.rb +42 -0
  43. data/spec/dummy/config/initializers/assets.rb +11 -0
  44. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  45. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  46. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  47. data/spec/dummy/config/initializers/inflections.rb +16 -0
  48. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  49. data/spec/dummy/config/initializers/session_store.rb +3 -0
  50. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  51. data/spec/dummy/config/locales/en.yml +23 -0
  52. data/spec/dummy/config/routes.rb +3 -0
  53. data/spec/dummy/config/secrets.yml +22 -0
  54. data/spec/dummy/db/migrate/20160927141815_create_timeful_activities.timeful.rb +17 -0
  55. data/spec/dummy/db/migrate/20160927155215_create_timeful_feed_items.timeful.rb +16 -0
  56. data/spec/dummy/db/migrate/20160927160026_create_users.rb +8 -0
  57. data/spec/dummy/db/migrate/20160927160031_create_posts.rb +8 -0
  58. data/spec/dummy/db/schema.rb +50 -0
  59. data/spec/dummy/log/development.log +361 -0
  60. data/spec/dummy/log/test.log +3554 -0
  61. data/spec/dummy/public/404.html +67 -0
  62. data/spec/dummy/public/422.html +67 -0
  63. data/spec/dummy/public/500.html +66 -0
  64. data/spec/dummy/public/favicon.ico +0 -0
  65. data/spec/examples.txt +12 -0
  66. data/spec/factories/posts.rb +5 -0
  67. data/spec/factories/timeful/activities.rb +8 -0
  68. data/spec/factories/timeful/feed_items.rb +7 -0
  69. data/spec/factories/users.rb +5 -0
  70. data/spec/jobs/timeful/deliver_activity_to_subscribers_job_spec.rb +17 -0
  71. data/spec/models/timeful/activity_spec.rb +8 -0
  72. data/spec/models/timeful/feed_item_spec.rb +8 -0
  73. data/spec/rails_helper.rb +21 -0
  74. data/spec/spec_helper.rb +73 -0
  75. data/spec/support/active_job.rb +3 -0
  76. data/spec/support/database_cleaner.rb +19 -0
  77. data/spec/support/factory_girl.rb +5 -0
  78. data/spec/support/faker.rb +1 -0
  79. data/spec/support/shoulda.rb +8 -0
  80. data/spec/timeful/model/actor_spec.rb +20 -0
  81. data/spec/timeful/model/subscriber_spec.rb +17 -0
  82. data/spec/timeful/relation_proxy_spec.rb +34 -0
  83. data/spec/timeful/timeful_spec.rb +12 -0
  84. metadata +337 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8f3483ad41973a14436a441189dc9ae9f6ad6a40
4
+ data.tar.gz: 008af65f6a63f6dd6e8ffead55c67330e0d303b8
5
+ SHA512:
6
+ metadata.gz: f2044cb33f76b7e6db9fb08cc25a62eff75c48d54eb4aece32a2abb9d3c720a924316659ee76021639999d4723478b039715a0caae8905794e179f55e5d2b201
7
+ data.tar.gz: 7fd5b9e367d372efb26d57f568579e16ff6457473a1d28b0fb158f9354f68bb0b50aff0dec3eb7b35edb22d0612d864698bae9e7396e66baf3923a9fc1668f52
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2016 Alessandro Desantis
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,112 @@
1
+ # Timeful
2
+
3
+ [![Gem Version](https://img.shields.io/gem/v/timeful.svg?maxAge=3600&style=flat-square)](https://rubygems.org/gems/timeful)
4
+ [![Dependency Status](https://img.shields.io/gemnasium/alessandro1997/timeful.svg?maxAge=3600&style=flat-square)](https://gemnasium.com/github.com/alessandro1997/timeful)
5
+ [![Code Climate](https://img.shields.io/codeclimate/github/alessandro1997/timeful.svg?maxAge=3600&style=flat-square)](https://codeclimate.com/github/alessandro1997/timeful)
6
+
7
+ Timeful is a Ruby on Rails engine for building timelines (aka "activity streams").
8
+
9
+ ## Why another gem?
10
+
11
+ There are battle-tested activity stream builders out there already (the most known being
12
+ [PublicActivity][public_activity], [TimelineFu][timeline_fu] and [Social Stream][social_stream].
13
+ However, these gems do not really create a feed for each user, but simply record a list of global
14
+ activities and leave you to deal with the retrieval.
15
+
16
+ Timeful is different: it takes a simple approach at building user feeds and allows you to obtain
17
+ an ordered list of feed items for each user.
18
+
19
+ Also, Timeful does not handle presentation: you will have to build your own views and controllers to
20
+ expose feeds. This keeps the codebase smaller and allows you to easily integrate Timeful in JSON
21
+ APIs.
22
+
23
+ [public_activity]: https://github.com/chaps-io/public_activity
24
+ [timeline_fu]: https://github.com/jamesgolick/timeline_fu
25
+ [social_stream]: https://github.com/ging/social_stream
26
+
27
+ ## Installation
28
+
29
+ Add this line to your application's Gemfile:
30
+
31
+ ```ruby
32
+ gem 'timeful'
33
+ ```
34
+
35
+ And then execute:
36
+
37
+ ```console
38
+ $ bundle
39
+ ```
40
+
41
+ Or install it yourself as:
42
+
43
+ ```console
44
+ $ gem install timeful
45
+ ```
46
+
47
+ Then, run the installer generator:
48
+
49
+ ```console
50
+ $ rails g timeful:install
51
+ $ rake db:migrate
52
+ ```
53
+
54
+ Finally, add the following to the model that will have a feed and publish activities (this is
55
+ usually your `User` model, but you can use two different models):
56
+
57
+ ```ruby
58
+ class User < ActiveRecord::Base
59
+ include Timeful::Model::Actor
60
+ include Timeful::Model::Subscriber
61
+ end
62
+ ```
63
+
64
+ ## Usage
65
+
66
+ Timeful revolves around three core concepts:
67
+
68
+ - **Activity**: An _action_ taken by an _actor_ on an _object_. _Metadata_ can also be attached to
69
+ activities. An example would be "John Doe (actor) wrote (action) a comment (object)."
70
+ - **Feed**: A collection of activities that should be accessible by a specific user.
71
+ - **Feed item**: The instance of an activity in a user's feed.
72
+
73
+ Each activity action (or "type") has its own class. This is required because Timeful has to know
74
+ which feeds the activity should be added to.
75
+
76
+ To avoid polluting `app/models`, it is recommended to put your activities in the `app/activities`
77
+ directory.
78
+
79
+ Here's an example activity:
80
+
81
+ ```ruby
82
+ class CommentActivity < Timeful::Activity
83
+ def subscribers
84
+ [object.post.author]
85
+ end
86
+ end
87
+ ```
88
+
89
+ Now, you can publish the `comment` activity:
90
+
91
+ ```ruby
92
+ user.publish_activity :comment, object: comment
93
+ ```
94
+
95
+ This will create an `Activity` and link it to the author's feed through a `FeedItem`:
96
+
97
+ ```ruby
98
+ author = comment.post.author
99
+ author.feed_items.count # => 1
100
+ ```
101
+
102
+ ## Performance
103
+
104
+ TODO: Write performance considerations
105
+
106
+ ## Contributing
107
+
108
+ Bug reports and pull requests are welcome on GitHub at https://github.com/alessandro1997/timeful.
109
+
110
+ ## License
111
+
112
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,31 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+ require 'rake/testtask'
9
+
10
+ RDoc::Task.new(:rdoc) do |rdoc|
11
+ rdoc.rdoc_dir = 'rdoc'
12
+ rdoc.title = 'Timeful'
13
+ rdoc.options << '--line-numbers'
14
+ rdoc.rdoc_files.include('README.rdoc')
15
+ rdoc.rdoc_files.include('lib/**/*.rb')
16
+ end
17
+
18
+ APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
19
+ load 'rails/tasks/engine.rake'
20
+ load 'rails/tasks/statistics.rake'
21
+
22
+ Bundler::GemHelper.install_tasks
23
+
24
+ Rake::TestTask.new(:spec) do |t|
25
+ t.libs << 'lib'
26
+ t.libs << 'spec'
27
+ t.pattern = 'spec/**/*_spec.rb'
28
+ t.verbose = false
29
+ end
30
+
31
+ task default: :spec
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+ module Timeful
3
+ # Creates a feed item for each subscriber of the activity.
4
+ #
5
+ # @author Alessandro Desantis
6
+ class DeliverActivityToSubscribersJob < ActiveJob::Base
7
+ queue_as { Timeful.jobs_queue }
8
+
9
+ def perform(activity)
10
+ RelationProxy.new(activity.subscribers).find_each do |subscriber|
11
+ subscriber.feed_items.create!(activity: activity)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+ module Timeful
3
+ # An activity is something that happens in your application. It's comprised of an actor, an
4
+ # action and an object.
5
+ #
6
+ # An example activity might be: "John Doe (actor) published (action) a new post (object)."
7
+ #
8
+ # @author Alessandro Desantis
9
+ #
10
+ # @abstract Subclass and override {#subscribers} to implement your own activity
11
+ #
12
+ # @example
13
+ # class CommentCreatedActivity < Timeful::Activity
14
+ # def subscribers
15
+ # [object.post.author]
16
+ # end
17
+ # end
18
+ class Activity < ApplicationRecord
19
+ belongs_to :actor, polymorphic: true
20
+ belongs_to :object, polymorphic: true
21
+
22
+ # Returns the users that subscribe to this activity. A {FeedItem} linked to the activity will
23
+ # be created for these users.
24
+ #
25
+ # The returned value can be an instance of either +ActiveRecord::Relation+ or +Array+. The
26
+ # former is preferred when the subscribers list is very long, as we'll automatically use
27
+ # +find_each+ to reduce memory usage if it's available.
28
+ #
29
+ # @raise NotImplementedError
30
+ def subscribers
31
+ fail NotImplementedError
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+ module Timeful
3
+ class ApplicationRecord < ActiveRecord::Base # rubocop:disable Style/Documentation
4
+ self.abstract_class = true
5
+ end
6
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+ module Timeful
3
+ # A feed item is an instance of an activity in a specific user's feed.
4
+ #
5
+ # Feed items are automatically created by Timeful when an activity is created for all subscribers
6
+ # of the activty.
7
+ #
8
+ # @author Alessandro Desantis
9
+ class FeedItem < ApplicationRecord
10
+ belongs_to :feedable, polymorphic: true
11
+ belongs_to :activity
12
+ end
13
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,2 @@
1
+ Timeful::Engine.routes.draw do
2
+ end
@@ -0,0 +1,16 @@
1
+ class CreateTimefulActivities < ActiveRecord::Migration[5.0]
2
+ def change
3
+ create_table :timeful_activities do |t|
4
+ t.string :type, null: false
5
+ t.string :object_type, null: false
6
+ t.integer :object_id, null: false
7
+ t.string :actor_type, null: false
8
+ t.integer :actor_id, null: false
9
+
10
+ t.timestamps null: false
11
+
12
+ t.index [:object_type, :object_id]
13
+ t.index [:actor_type, :actor_id]
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ class CreateTimefulFeedItems < ActiveRecord::Migration[5.0]
2
+ def change
3
+ create_table :timeful_feed_items do |t|
4
+ t.string :feedable_type, null: false
5
+ t.integer :feedable_id, null: false
6
+ t.integer :activity_id, null: false
7
+
8
+ t.timestamps null: false
9
+
10
+ t.foreign_key :timeful_activities, column: :activity_id, on_delete: :cascade
11
+
12
+ t.index [:feedable_type, :feedable_id]
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+ module Timeful
3
+ class InstallGenerator < Rails::Generators::Base # rubocop:disable Style/Documentation
4
+ source_root File.expand_path('../templates', __FILE__)
5
+
6
+ def copy_initializer_file
7
+ rake 'timeful:install:migrations'
8
+ end
9
+ end
10
+ end
data/lib/timeful.rb ADDED
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+ require 'timeful/version'
3
+
4
+ require 'timeful/relation_proxy'
5
+
6
+ require 'timeful/model/actor'
7
+ require 'timeful/model/subscriber'
8
+
9
+ require 'timeful/engine'
10
+
11
+ module Timeful # rubocop:disable Style/Documentation
12
+ # !@attribute [rw]
13
+ # @return [Symbol] the queue to use for background jobs (default is +default+)
14
+ mattr_accessor :jobs_queue
15
+ self.jobs_queue = :default
16
+
17
+ # Yields the module for configuration.
18
+ #
19
+ # @yieldparam config [Timeful] the +Timeful+ module
20
+ #
21
+ # @example
22
+ # Timeful.configure do |config|
23
+ # config.jobs_queue = :timeful
24
+ # end
25
+ def self.configure
26
+ yield self
27
+ end
28
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+ module Timeful
3
+ class Engine < ::Rails::Engine # rubocop:disable Style/Documentation
4
+ isolate_namespace Timeful
5
+ end
6
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+ module Timeful
3
+ module Model
4
+ # An actor can publish activities.
5
+ #
6
+ # @author Alessandro Desantis
7
+ module Actor
8
+ def self.included(klass)
9
+ klass.include InstanceMethods
10
+ end
11
+
12
+ module InstanceMethods # rubocop:disable Style/Documentation
13
+ # Publishes an activity and creates a feed item for each subscriber.
14
+ #
15
+ # @param action [Symbol] the action (or activity type) to create
16
+ # @param object [ActiveRecord::Base] the object the action was taken upon
17
+ #
18
+ # @return [Activity] the created activity
19
+ #
20
+ # @example
21
+ # actor.publish_activity :post_created, object: post # => #<PostCreatedActivity>
22
+ def publish_activity(action, object:)
23
+ activity = activity_klass(action).create! object: object, actor: self
24
+ DeliverActivityToSubscribersJob.perform_later activity
25
+ end
26
+
27
+ private
28
+
29
+ def activity_klass(action)
30
+ "#{action.to_s.camelize}Activity".constantize
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+ module Timeful
3
+ module Model
4
+ # A subscriber can subscribe to activities and get a new feed item for each new instance of
5
+ # that activity.
6
+ #
7
+ # @author Alessandro Desantis
8
+ #
9
+ # @example
10
+ # subscriber.feed_items # => #<ActiveRecord::Associations::CollectionProxy>
11
+ module Subscriber
12
+ def self.included(klass)
13
+ klass.class_eval do
14
+ has_many :feed_items, -> { order(created_at: :desc) },
15
+ as: :feedable,
16
+ class_name: 'Timeful::FeedItem'
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+ module Timeful
3
+ # Makes +Array+ behave much like +ActiveRecord::Relation+.
4
+ #
5
+ # This proxy class wraps an +Array+ or +ActiveRecord::Relation+ instance and makes the former
6
+ # behave like the latter by simulating methods like +find_each+.
7
+ #
8
+ # @author Alessandro Desantis
9
+ class RelationProxy < SimpleDelegator
10
+ # If +find_each+ is defined on the object we're delegating to (i.e. the object is an instance
11
+ # of +ActiveRecord::Relation+), calls it on the object.
12
+ #
13
+ # Otherwise, calls +each+ on the object and yields the values.
14
+ #
15
+ # @yieldparam item [Object] the items in the object
16
+ def find_each(*args, &block)
17
+ method = if __getobj__.respond_to?(:find_each)
18
+ :find_each
19
+ else
20
+ :each
21
+ end
22
+
23
+ __getobj__.send(method, *args, &block)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ module Timeful
3
+ VERSION = '0.0.1'
4
+ end
@@ -0,0 +1,28 @@
1
+ == README
2
+
3
+ This README would normally document whatever steps are necessary to get the
4
+ application up and running.
5
+
6
+ Things you may want to cover:
7
+
8
+ * Ruby version
9
+
10
+ * System dependencies
11
+
12
+ * Configuration
13
+
14
+ * Database creation
15
+
16
+ * Database initialization
17
+
18
+ * How to run the test suite
19
+
20
+ * Services (job queues, cache servers, search engines, etc.)
21
+
22
+ * Deployment instructions
23
+
24
+ * ...
25
+
26
+
27
+ Please feel free to use a different markup language if you do not plan to run
28
+ <tt>rake doc:app</tt>.