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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +112 -0
- data/Rakefile +31 -0
- data/app/jobs/timeful/deliver_activity_to_subscribers_job.rb +15 -0
- data/app/models/timeful/activity.rb +34 -0
- data/app/models/timeful/application_record.rb +6 -0
- data/app/models/timeful/feed_item.rb +13 -0
- data/config/routes.rb +2 -0
- data/db/migrate/20160927141608_create_timeful_activities.rb +16 -0
- data/db/migrate/20160927154912_create_timeful_feed_items.rb +15 -0
- data/lib/generators/timeful/install_generator.rb +10 -0
- data/lib/timeful.rb +28 -0
- data/lib/timeful/engine.rb +6 -0
- data/lib/timeful/model/actor.rb +35 -0
- data/lib/timeful/model/subscriber.rb +21 -0
- data/lib/timeful/relation_proxy.rb +26 -0
- data/lib/timeful/version.rb +4 -0
- data/spec/dummy/README.rdoc +28 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/activities/post_activity.rb +5 -0
- data/spec/dummy/app/assets/javascripts/application.js +13 -0
- data/spec/dummy/app/assets/stylesheets/application.css +15 -0
- data/spec/dummy/app/controllers/application_controller.rb +5 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/models/application_record.rb +3 -0
- data/spec/dummy/app/models/post.rb +2 -0
- data/spec/dummy/app/models/user.rb +4 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/bin/setup +29 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +22 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.example.yml +19 -0
- data/spec/dummy/config/database.yml +19 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +41 -0
- data/spec/dummy/config/environments/production.rb +79 -0
- data/spec/dummy/config/environments/test.rb +42 -0
- data/spec/dummy/config/initializers/assets.rb +11 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +23 -0
- data/spec/dummy/config/routes.rb +3 -0
- data/spec/dummy/config/secrets.yml +22 -0
- data/spec/dummy/db/migrate/20160927141815_create_timeful_activities.timeful.rb +17 -0
- data/spec/dummy/db/migrate/20160927155215_create_timeful_feed_items.timeful.rb +16 -0
- data/spec/dummy/db/migrate/20160927160026_create_users.rb +8 -0
- data/spec/dummy/db/migrate/20160927160031_create_posts.rb +8 -0
- data/spec/dummy/db/schema.rb +50 -0
- data/spec/dummy/log/development.log +361 -0
- data/spec/dummy/log/test.log +3554 -0
- data/spec/dummy/public/404.html +67 -0
- data/spec/dummy/public/422.html +67 -0
- data/spec/dummy/public/500.html +66 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/examples.txt +12 -0
- data/spec/factories/posts.rb +5 -0
- data/spec/factories/timeful/activities.rb +8 -0
- data/spec/factories/timeful/feed_items.rb +7 -0
- data/spec/factories/users.rb +5 -0
- data/spec/jobs/timeful/deliver_activity_to_subscribers_job_spec.rb +17 -0
- data/spec/models/timeful/activity_spec.rb +8 -0
- data/spec/models/timeful/feed_item_spec.rb +8 -0
- data/spec/rails_helper.rb +21 -0
- data/spec/spec_helper.rb +73 -0
- data/spec/support/active_job.rb +3 -0
- data/spec/support/database_cleaner.rb +19 -0
- data/spec/support/factory_girl.rb +5 -0
- data/spec/support/faker.rb +1 -0
- data/spec/support/shoulda.rb +8 -0
- data/spec/timeful/model/actor_spec.rb +20 -0
- data/spec/timeful/model/subscriber_spec.rb +17 -0
- data/spec/timeful/relation_proxy_spec.rb +34 -0
- data/spec/timeful/timeful_spec.rb +12 -0
- 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
|
+
[](https://rubygems.org/gems/timeful)
|
4
|
+
[](https://gemnasium.com/github.com/alessandro1997/timeful)
|
5
|
+
[](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,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,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,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,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>.
|