Repository: pokonski/public_activity Branch: main Commit: d8200889330c Files: 72 Total size: 99.7 KB Directory structure: gitextract_a19chaa2/ ├── .editorconfig ├── .github/ │ └── workflows/ │ └── main.yml ├── .gitignore ├── .travis.yml ├── Appraisals ├── CHANGELOG.md ├── Gemfile ├── MIT-LICENSE ├── README.md ├── Rakefile ├── gemfiles/ │ ├── .bundle/ │ │ └── config │ ├── rails_6.1.gemfile │ ├── rails_7.0.gemfile │ └── rails_7.1.gemfile ├── lib/ │ ├── generators/ │ │ ├── public_activity/ │ │ │ ├── migration/ │ │ │ │ ├── migration_generator.rb │ │ │ │ └── templates/ │ │ │ │ └── migration.rb │ │ │ └── migration_upgrade/ │ │ │ ├── migration_upgrade_generator.rb │ │ │ └── templates/ │ │ │ └── upgrade.rb │ │ └── public_activity.rb │ ├── public_activity/ │ │ ├── actions/ │ │ │ ├── creation.rb │ │ │ ├── destruction.rb │ │ │ └── update.rb │ │ ├── activity.rb │ │ ├── common.rb │ │ ├── config.rb │ │ ├── models/ │ │ │ ├── activist.rb │ │ │ ├── activity.rb │ │ │ ├── adapter.rb │ │ │ └── trackable.rb │ │ ├── orm/ │ │ │ ├── active_record/ │ │ │ │ ├── activist.rb │ │ │ │ ├── activity.rb │ │ │ │ ├── adapter.rb │ │ │ │ └── trackable.rb │ │ │ ├── active_record.rb │ │ │ ├── mongo_mapper/ │ │ │ │ ├── activist.rb │ │ │ │ ├── activity.rb │ │ │ │ ├── adapter.rb │ │ │ │ └── trackable.rb │ │ │ ├── mongo_mapper.rb │ │ │ ├── mongoid/ │ │ │ │ ├── activist.rb │ │ │ │ ├── activity.rb │ │ │ │ ├── adapter.rb │ │ │ │ └── trackable.rb │ │ │ └── mongoid.rb │ │ ├── renderable.rb │ │ ├── roles/ │ │ │ ├── deactivatable.rb │ │ │ └── tracked.rb │ │ ├── testing.rb │ │ ├── utility/ │ │ │ ├── store_controller.rb │ │ │ └── view_helpers.rb │ │ └── version.rb │ └── public_activity.rb ├── public_activity.gemspec ├── public_activity.sublime-project └── test/ ├── migrations/ │ ├── 002_create_articles.rb │ ├── 003_create_users.rb │ └── 004_add_nonstandard_to_activities.rb ├── mongo_mapper.yml ├── mongoid.yml ├── test_activist.rb ├── test_activity.rb ├── test_common.rb ├── test_controller_integration.rb ├── test_generators.rb ├── test_helper.rb ├── test_testing.rb ├── test_tracking.rb ├── test_view_helpers.rb └── views/ ├── custom/ │ ├── _layout.erb │ └── _test.erb ├── layouts/ │ └── _activity.erb └── public_activity/ └── _test.erb ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.{json,yml}] indent_size = 2 [*.{diff,md}] trim_trailing_whitespace = false ================================================ FILE: .github/workflows/main.yml ================================================ name: CI on: pull_request: paths-ignore: - 'README.md' push: paths-ignore: - 'README.md' workflow_dispatch: jobs: test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: ruby_version: ['3.0', '3.1', '3.2', '3.3'] rails_version: ['6.1', '7.0', '7.1'] exclude: - ruby_version: 3.1 rails_version: 6.1 - ruby_version: 3.2 rails_version: 6.1 - ruby_version: 3.3 rails_version: 6.1 steps: - uses: actions/checkout@v4 - name: Setup Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby_version }} - name: Build and run test run: | bundle bundle exec appraisal rails_${{ matrix.rails_version }} bundle bundle exec appraisal rails_${{ matrix.rails_version }} rake ================================================ FILE: .gitignore ================================================ /doc/ /.yardoc/ *.gem /coverage/ /*.sublime-workspace /tmp /Gemfile.lock /gemfiles/*.lock ================================================ FILE: .travis.yml ================================================ language: ruby rvm: - 2.2.6 - 2.3.3 matrix: include: - rvm: 2.2.6 gemfile: gemfiles/Gemfile.rails-4.0 env: PA_ORM=active_record - rvm: 2.3.3 gemfile: gemfiles/Gemfile.rails-4.0 env: PA_ORM=active_record - rvm: 2.3.3 gemfile: gemfiles/Gemfile.rails-5.0 env: PA_ORM=active_record - rvm: 2.5.1 gemfile: gemfiles/Gemfile.rails-5.2 env: PA_ORM=active_record - rvm: 2.2.6 env: PA_ORM=mongoid env: - PA_ORM=active_record - PA_ORM=mongo_mapper services: - mongodb email: recipients: - piotrek@okonski.org on_success: change on_failure: always ================================================ FILE: Appraisals ================================================ # frozen_string_literal: true if RUBY_VERSION.to_f < 3.1 appraise 'rails_6.1' do gem 'rails', '~> 6.1.0' gem 'openssl' end end appraise 'rails_7.0' do gem 'rails', '~> 7.0.1' end appraise 'rails_7.1' do gem 'rails', '~> 7.1.0' end ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## 3.0.2 - **Fixed** Refactor prepare_parameters method to handle nil parameters (s. #387, thanks [Himalaya Pal](https://github.com/palhimalaya)) - **Fixed** CI failure due to lax sqlite3 version constraint (s. #389, thanks [Junichi Sato](https://github.com/sato11)) ## 3.0.1 - **Fixed** Rails 6.1/7.0 regression in serialization of `nil`/`NULL` values - **Fixed** Docs for `ORM::ActiveRecord::Activist` ## 3.0.0 - **Added** Rails 7.1 support (s. #384, thanks [max.jos](https://github.com/yhru)) - **Removed** Ruby <= 2.7 support - **Removed** Rails <= 6.0 support ## 2.0.2 - **Fixed** Rescue from `ActiveRecord::ConnectionNotEstablished` (s. #372, thanks [Gabe Blair](https://github.com/gblair) & [Vitalie Lazu](https://github.com/vitaliel)) ## 2.0.1 - **Fixed** Fix regression in generated migration (s. #368, thanks [Colin Bonner](https://github.com/cfbonner)) ## 2.0.0 - **Fixed** Deprecation warnings in Ruby 2.7/3.0 due to double splat operator - **Fixed** Ruby warnings due to unused variables `e` in exception handling - **Added** Ruby 3.1 support - **Added** Rails 7.0 support - **Removed** Ruby <= 2.4 support - **Removed** Rails <= 4.2 support ## 1.6.4 - **Fixed** exception when not using MySQL or Postgres (see #335, thanks to [Roland Netzsch](https://github.com/stuxcrystal)) - **Added** `create_activity!` method which raises exception on failures, much like `save!` in ActiveRecord (see #334, thanks to [Jonathan](https://github.com/jtwhittington)) - **Added** support for ActiveRecord 6 by whitelisting it (see #332, thanks to [Emre Demir](https://github.com/demir)) - **Added** frozen_string_literal pragma to Ruby files for better performance (see #329, thanks to [Krzysztof Rybka](https://github.com/krzysiek1507)) ## 1.6.3 - **Fixed** a bug which resulted in crashes when PostgreSQL connection failed (see #328, thanks to [Ken Greeff](https://github.com/kengreeff)) - **Fixed** a bug which resulted in crashes when MySQL connection failed (see #327, thanks to [Miquel Sabaté Solà](https://github.com/mssola)) ## 1.6.2 - **Fixed** a bug which resulted in crashes when database didn't exist (see #323, thanks to [Anmol Chopra](https://github.com/chopraanmol1)) ## 1.6.1 - **Fixed** a bug with requiring not existent file in generated migrations ## 1.6.0 * **Add support for Rails 5.2** * Make config settings thread safe (fixes #284) * Fix Rspec tests failing in Rails 5 due to ViewHelpers (see #271, thanks to [Janusz](https://github.com/januszm)) * Support JSON, JSONB and HSTORE types for `parameters` column (see #285, thanks to [djvs](https://github.com/djvs) and #315 (thanks to [mateusg](https://github.com/mateusg)) ## 1.5.0 * Refactor PublicActivity::StoreController to rely on Thread.current instead. (see #252, thanks to [Ryan McGeary](https://github.com/rmm5t)) * Remove support for Ruby 1.9.3 and Ruby 2.0.0 (thanks to [Ryan McGeary](https://github.com/rmm5t)) ## 1.4.2 * Fix bug with migrations not having an extension in ActiveRecord >= 4.0.0 ## 1.4.1 * Fixed issue with Rails 4 when using ProtectedAttributes gem (see #128) * General code clean-ups. ## 1.4.0 * Added support for MongoMapper ORM (thanks to [Julio Olivera](https://github.com/julioolvr)) [PR](https://github.com/pokonski/public_activity/pull/101) * Added support for stable **Rails 4.0** while keeping compatibility with Rails 3.X * `render_activity` can now render collections of activities instead of just a single one. Also aliased as `render_activities` * Fix issue in rendering multiple activities when options were incomplete for every subsequent activity after the first one * `render_activity` now accetps `:locals` option. Works the same way as `:locals` for Rails `render` method. ## 1.1.0 * Fixed an issue when AR was loading despite choosing Mongoid in multi-ORM Rails applications (thanks to [Robert Ulejczyk](https://github.com/robuye)) ## 1.0.3 * Fixed a bug which modified globals (thanks to [Weera Wu](https://github.com/wulab)) ## 1.0.2 * Fixed undefined constant PublicActivity::Activity for Activist associations (thanks to [Стас Сушков](https://github.com/stas)) ## 1.0.1 * #create_activity now correctly returns activity object. * Fixed :owner not being set correctly when passed to #create_activity (thanks to [Drew Miller](https://github.com/mewdriller)) ## 1.0 (released 10/02/2013) * **Now supports Mongoid 3 and Active Record.** * Added indexes for polymorphic column pairs to speed up queries in ActiveRecord * `#create_activity` now returns the newly created Activity object * Support for custom Activity attributes. Now if you need a custom relation for Activities you can create a migration which adds the desired column, whitelist the attribute, and then you can simply pass the value to #create_activity * `#tracked` can now accept a single Symbol for its `:only` and `:except` options. * It is now possible to include `PublicActivity::Common` in your models if you just want to use `#create_activity` method and skip the default CRUD tracking. * `#render_activity` now accepts Symbols or Strings for :layout parameter. ### Example ```ruby # All look for app/views/layouts/_activity.erb render_activity @activity, :layout => "activity" render_activity @activity, :layout => "layouts/activity" render_activity @activity, :layout => :activity ``` ## 0.5.4 * Fixed support for namespaced classes when transforming into view path. For example `MyNamespace::CamelCase` now correctly transforms to key: `my_namespace_camel_case.create` ================================================ FILE: Gemfile ================================================ # frozen_string_literal: true source "https://rubygems.org" gemspec ================================================ FILE: MIT-LICENSE ================================================ Copyright (c) 2011-2013 Piotrek Okoński Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # PublicActivity [![Code Climate](https://codeclimate.com/github/chaps-io/public_activity.svg)](https://codeclimate.com/github/chaps-io/public_activity) [![Gem Version](https://badge.fury.io/rb/public_activity.svg)](http://badge.fury.io/rb/public_activity) `public_activity` provides easy activity tracking for your **ActiveRecord**, **Mongoid 3** and **MongoMapper** models in Rails 6.1+. Simply put: it records what has been changed or created and gives you the ability to present those recorded activities to users - similarly to how GitHub does it. ## Table of contents - [Ruby/Rails version support](#ruby-rails-version-support) - [Table of contents](#table-of-contents) - [Example](#example) - [Online demo](#online-demo) - [Screencast](#screencast) - [Setup](#setup) - [Gem installation](#gem-installation) - [Database setup](#database-setup) - [Model configuration](#model-configuration) - [Custom activities](#custom-activities) - [Displaying activities](#displaying-activities) - [Layouts](#layouts) - [Locals](#locals) - [Activity views](#activity-views) - [I18n](#I18n) - [Testing](#testing) - [Documentation](#documentation) - [Common examples](#common-examples) - [Help](#help) - [License](#license) ## Ruby/Rails version support Version `~> 3.0`` supports Ruby 3.0+ and Rails 6.1+. For older Ruby versions (≤2.7) and Rails 5.0+ you can use version `~> 2.0` until you can upgrade to newer versions of Ruby + Rails. Issues related to those unsupported versions of Ruby/Rails will be closed without resolution and PRs will not be accepted. ## Example Here is a simple example showing what this gem is about: ![Example usage](http://i.imgur.com/q0TVx.png) ### Demo app The source code of the demo is hosted here: https://github.com/pokonski/activity_blog ## Screencast Ryan Bates made a [great screencast](http://railscasts.com/episodes/406-public-activity) describing how to integrate Public Activity in your Rails Application. ## Setup ### Gem installation You can install `public_activity` as you would any other gem: gem install public_activity or in your Gemfile: ```ruby gem 'public_activity' ``` ### Database setup By default _public_activity_ uses Active Record. If you want to use Mongoid or MongoMapper as your backend, create an initializer file in your Rails application with the corresponding code inside: For _Mongoid:_ ```ruby # config/initializers/public_activity.rb PublicActivity::Config.set do orm :mongoid end ``` For _MongoMapper:_ ```ruby # config/initializers/public_activity.rb PublicActivity::Config.set do orm :mongo_mapper end ``` **(ActiveRecord only)** Create migration for activities and migrate the database (in your Rails project): rails g public_activity:migration rake db:migrate ### Model configuration Include `PublicActivity::Model` and add `tracked` to the model you want to keep track of: For _ActiveRecord:_ ```ruby class Article < ActiveRecord::Base include PublicActivity::Model tracked end ``` For _Mongoid:_ ```ruby class Article include Mongoid::Document include PublicActivity::Model tracked end ``` For _MongoMapper:_ ```ruby class Article include MongoMapper::Document include PublicActivity::Model tracked end ``` And now, by default create/update/destroy activities are recorded in activities table. This is all you need to start recording activities for basic CRUD actions. _Optional_: If you don't need `#tracked` but still want the comfort of `#create_activity`, you can include only the lightweight `Common` module instead of `Model`. #### Custom activities You can trigger custom activities by setting all your required parameters and triggering `create_activity` on the tracked model, like this: ```ruby @article.create_activity key: 'article.commented_on', owner: current_user ``` See this entry http://rubydoc.info/gems/public_activity/PublicActivity/Common:create_activity for more details. ### Displaying activities To display them you simply query the `PublicActivity::Activity` model: ```ruby # notifications_controller.rb def index @activities = PublicActivity::Activity.all end ``` And in your views: ```erb <%= render_activities(@activities) %> ``` *Note*: `render_activity` is a helper for use in view templates. `render_activity(activity)` can be written as `activity.render(self)` and it will have the same meaning. *Note*: `render_activities` is an alias for `render_activity` and does the same. #### Layouts You can also pass options to both `activity#render` and `#render_activity` methods, which are passed deeper to the internally used `render_partial` method. A useful example would be to render activities wrapped in layout, which shares common elements of an activity, like a timestamp, owner's avatar etc: ```erb <%= render_activities(@activities, layout: :activity) %> ``` The activity will be wrapped with the `app/views/layouts/_activity.erb` layout, in the above example. **Important**: please note that layouts for activities are also partials. Hence the `_` prefix. #### Locals Sometimes, it's desirable to pass additional local variables to partials. It can be done this way: ```erb <%= render_activity(@activity, locals: {friends: current_user.friends}) %> ``` *Note*: Before 1.4.0, one could pass variables directly to the options hash for `#render_activity` and access it from activity parameters. This functionality is retained in 1.4.0 and later, but the `:locals` method is **preferred**, since it prevents bugs from shadowing variables from activity parameters in the database. #### Activity views `public_activity` looks for views in `app/views/public_activity`. For example, if you have an activity with `:key` set to `"activity.user.changed_avatar"`, the gem will look for a partial in `app/views/public_activity/user/_changed_avatar.(erb|haml|slim|something_else)`. *Hint*: the `"activity."` prefix in `:key` is completely optional and kept for backwards compatibility, you can skip it in new projects. If a view file does not exist, then p_a falls back to the old behaviour and tries to translate the activity `:key` using `I18n#translate` method (see the section below). #### I18n Translations are used by the `#text` method, to which you can pass additional options in form of a hash. `#render` method uses translations when view templates have not been provided. You can render pure i18n strings by passing `{display: :i18n}` to `#render_activity` or `#render`. Translations should be put in your locale `.yml` files. To render pure strings from I18n Example structure: ```yaml activity: article: create: 'Article has been created' update: 'Someone has edited the article' destroy: 'Some user removed an article!' ``` This structure is valid for activities with keys `"activity.article.create"` or `"article.create"`. As mentioned before, `"activity."` part of the key is optional. ## Testing For RSpec you can first disable `public_activity` and add the `test_helper` in `rails_helper.rb` with: ```ruby #rails_helper.rb require 'public_activity/testing' PublicActivity.enabled = false ``` In your specs you can then blockwise decide whether to turn `public_activity` on or off. ```ruby # file_spec.rb PublicActivity.with_tracking do # your test code goes here end PublicActivity.without_tracking do # your test code goes here end ``` ## Documentation For more documentation go [here](http://rubydoc.info/gems/public_activity/index) ## Common examples * [[How to] Set the Activity's owner to current_user by default](https://github.com/pokonski/public_activity/wiki/%5BHow-to%5D-Set-the-Activity's-owner-to-current_user-by-default) * [[How to] Disable tracking for a class or globally](https://github.com/pokonski/public_activity/wiki/%5BHow-to%5D-Disable-tracking-for-a-class-or-globally) * [[How to] Create custom activities](https://github.com/pokonski/public_activity/wiki/%5BHow-to%5D-Create-custom-activities) * [[How to] Use custom fields on Activity](https://github.com/pokonski/public_activity/wiki/%5BHow-to%5D-Use-custom-fields-on-Activity) ## Help If you need help with using public_activity please visit our discussion group and ask a question there: https://groups.google.com/forum/?fromgroups#!forum/public-activity Please do not ask general questions in the Github Issues. ## License Copyright (c) 2011-2013 Piotrek Okoński, released under the MIT license ================================================ FILE: Rakefile ================================================ # frozen_string_literal: true require 'bundler/gem_tasks' require 'rake' require 'yard' require 'yard/rake/yardoc_task' require 'rake/testtask' task default: :test desc 'Generate documentation for the public_activity plugin.' YARD::Rake::YardocTask.new do |doc| doc.files = ['lib/**/*.rb'] end Rake::TestTask.new do |t| t.libs << 'test' t.test_files = FileList['test/test*.rb'] end ================================================ FILE: gemfiles/.bundle/config ================================================ --- BUNDLE_RETRY: "1" ================================================ FILE: gemfiles/rails_6.1.gemfile ================================================ # This file was generated by Appraisal source "https://rubygems.org" gem "rails", "~> 6.1.0" gem "openssl" gemspec path: "../" ================================================ FILE: gemfiles/rails_7.0.gemfile ================================================ # This file was generated by Appraisal source "https://rubygems.org" gem "rails", "~> 7.0.1" gemspec path: "../" ================================================ FILE: gemfiles/rails_7.1.gemfile ================================================ # This file was generated by Appraisal source "https://rubygems.org" gem "rails", "~> 7.1.0" gemspec path: "../" ================================================ FILE: lib/generators/public_activity/migration/migration_generator.rb ================================================ # frozen_string_literal: true require 'generators/public_activity' require 'rails/generators/active_record' module PublicActivity module Generators # Migration generator that creates migration file from template class MigrationGenerator < ActiveRecord::Generators::Base extend Base argument :name, :type => :string, :default => 'create_activities' # Create migration in project's folder def generate_files migration_template 'migration.rb', "db/migrate/#{name}.rb" end end end end ================================================ FILE: lib/generators/public_activity/migration/templates/migration.rb ================================================ # frozen_string_literal: true # Migration responsible for creating a table with activities class CreateActivities < ActiveRecord::Migration[6.1] def self.up create_table :activities do |t| t.belongs_to :trackable, polymorphic: true t.belongs_to :owner, polymorphic: true t.string :key t.text :parameters t.belongs_to :recipient, polymorphic: true t.timestamps end end # Drop table def self.down drop_table :activities end end ================================================ FILE: lib/generators/public_activity/migration_upgrade/migration_upgrade_generator.rb ================================================ # frozen_string_literal: true require 'generators/public_activity' require 'rails/generators/active_record' module PublicActivity module Generators # Migration generator that creates migration file from template class MigrationUpgradeGenerator < ActiveRecord::Generators::Base extend Base argument :name, :type => :string, :default => 'upgrade_activities' # Create migration in project's folder def generate_files migration_template 'upgrade.rb', "db/migrate/#{name}.rb" end end end end ================================================ FILE: lib/generators/public_activity/migration_upgrade/templates/upgrade.rb ================================================ # frozen_string_literal: true # Migration responsible for creating a table with activities class UpgradeActivities < ActiveRecord::Migration[5.0] # Create table def self.change change_table :activities do |t| t.belongs_to :recipient, :polymorphic => true end end end ================================================ FILE: lib/generators/public_activity.rb ================================================ # frozen_string_literal: true require 'rails/generators/named_base' module PublicActivity # A generator module with Activity table schema. module Generators # A base module module Base # Get path for migration template def source_root @_public_activity_source_root ||= File.expand_path(File.join('../public_activity', generator_name, 'templates'), __FILE__) end end end end ================================================ FILE: lib/public_activity/actions/creation.rb ================================================ # frozen_string_literal: true module PublicActivity # Handles creation of Activities upon destruction and update of tracked model. module Creation extend ActiveSupport::Concern included do after_create :activity_on_create end private # Creates activity upon creation of the tracked model def activity_on_create create_activity(:create) end end end ================================================ FILE: lib/public_activity/actions/destruction.rb ================================================ # frozen_string_literal: true module PublicActivity # Handles creation of Activities upon destruction of tracked model. module Destruction extend ActiveSupport::Concern included do before_destroy :activity_on_destroy end private # Records an activity upon destruction of the tracked model def activity_on_destroy create_activity(:destroy) end end end ================================================ FILE: lib/public_activity/actions/update.rb ================================================ # frozen_string_literal: true module PublicActivity # Handles creation of Activities upon destruction and update of tracked model. module Update extend ActiveSupport::Concern included do after_update :activity_on_update end private # Creates activity upon modification of the tracked model def activity_on_update # Either use #changed? method for Rails < 5.1 or #saved_changes? for recent versions create_activity(:update) if respond_to?(:saved_changes?) ? saved_changes? : changed? end end end ================================================ FILE: lib/public_activity/activity.rb ================================================ # frozen_string_literal: true module PublicActivity # Main model, stores all information about what happened, # who caused it, when and anything else. class Activity < inherit_orm("Activity") end end ================================================ FILE: lib/public_activity/common.rb ================================================ # frozen_string_literal: true module PublicActivity # Happens when creating custom activities without either action or a key. class NoKeyProvided < Exception; end # Used to smartly transform value from metadata to data. # Accepts Symbols, which it will send against context. # Accepts Procs, which it will execute with controller and context. # @since 0.4.0 def self.resolve_value(context, thing) case thing when Symbol context.__send__(thing) when Proc thing.call(PublicActivity.get_controller, context) else thing end end # Common methods shared across the gem. module Common extend ActiveSupport::Concern included do include Trackable class_attribute :activity_owner_global, :activity_recipient_global, :activity_params_global, :activity_hooks, :activity_custom_fields_global set_public_activity_class_defaults end # @!group Global options # @!attribute activity_owner_global # Global version of activity owner # @see #activity_owner # @return [Model] # @!attribute activity_recipient_global # Global version of activity recipient # @see #activity_recipient # @return [Model] # @!attribute activity_params_global # Global version of activity parameters # @see #activity_params # @return [Hash] # @!attribute activity_hooks # @return [Hash] # Hooks/functions that will be used to decide *if* the activity should get # created. # # The supported keys are: # * :create # * :update # * :destroy # @!endgroup # @!group Instance options # Set or get parameters that will be passed to {Activity} when saving # # == Usage: # # @article.activity_params = {:article_title => @article.title} # @article.save # # This way you can pass strings that should remain constant, even when model attributes # change after creating this {Activity}. # @return [Hash] attr_accessor :activity_params @activity_params = {} # Set or get owner object responsible for the {Activity}. # # == Usage: # # # where current_user is an object of logged in user # @article.activity_owner = current_user # # OR: take @article.author association # @article.activity_owner = :author # # OR: provide a Proc with custom code # @article.activity_owner = proc {|controller, model| model.author } # @article.save # @article.activities.last.owner #=> Returns owner object # @return [Model] Polymorphic model # @see #activity_owner_global attr_accessor :activity_owner @activity_owner = nil # Set or get recipient for activity. # # Association is polymorphic, thus allowing assignment of # all types of models. This can be used for example in the case of sending # private notifications for only a single user. # @return (see #activity_owner) attr_accessor :activity_recipient @activity_recipient = nil # Set or get custom i18n key passed to {Activity}, later used in {Renderable#text} # # == Usage: # # @article = Article.new # @article.activity_key = "my.custom.article.key" # @article.save # @article.activities.last.key #=> "my.custom.article.key" # # @return [String] attr_accessor :activity_key @activity_key = nil # Set or get custom fields for later processing # # @return [Hash] attr_accessor :activity_custom_fields @activity_custom_fields = {} # @!visibility private @@activity_hooks = {} # @!endgroup # Provides some global methods for every model class. class_methods do # # @since 1.0.0 # @api private def set_public_activity_class_defaults self.activity_owner_global = nil self.activity_recipient_global = nil self.activity_params_global = {} self.activity_hooks = {} self.activity_custom_fields_global = {} end # Extracts a hook from the _:on_ option provided in # {Tracked::ClassMethods#tracked}. Returns nil when no hook exists for # given action # {Common#get_hook} # # @see Tracked#get_hook # @param key [String, Symbol] action to retrieve a hook for # @return [Proc, nil] callable hook or nil # @since 0.4.0 # @api private def get_hook(key) key = key.to_sym if activity_hooks.key?(key) && activity_hooks[key].is_a?(Proc) activity_hooks[key] end end end # # Returns true if PublicActivity is enabled # globally and for this class. # @return [Boolean] # @api private # @since 0.5.0 def public_activity_enabled? PublicActivity.enabled? end # # Shortcut for {ClassMethods#get_hook} # @param (see ClassMethods#get_hook) # @return (see ClassMethods#get_hook) # @since (see ClassMethods#get_hook) # @api (see ClassMethods#get_hook) def get_hook(key) self.class.get_hook(key) end # Calls hook safely. # If a hook for given action exists, calls it with model (self) and # controller (if available, see {StoreController}) # @param key (see #get_hook) # @return [Boolean] if hook exists, it's decision, if there's no hook, true # @since 0.4.0 # @api private def call_hook_safe(key) hook = get_hook(key) if hook # provides hook with model and controller hook.call(self, PublicActivity.get_controller) else true end end # Directly creates activity record in the database, based on supplied options. # # It's meant for creating custom activities while *preserving* *all* # *configuration* defined before. If you fire up the simplest of options: # # current_user.create_activity(:avatar_changed) # # It will still gather data from any procs or symbols you passed as params # to {Tracked::ClassMethods#tracked}. It will ask the hooks you defined # whether to really save this activity. # # But you can also overwrite instance and global settings with your options: # # @article.activity :owner => proc {|controller| controller.current_user } # @article.create_activity(:commented_on, :owner => @user) # # And it's smart! It won't execute your proc, since you've chosen to # overwrite instance parameter _:owner_ with @user. # # [:key] # The key will be generated from either: # * the first parameter you pass that is not a hash (*action*) # * the _:action_ option in the options hash (*action*) # * the _:key_ option in the options hash (it has to be a full key, # including model name) # When you pass an *action* (first two options above), they will be # added to parameterized model name: # # Given Article model and instance: @article, # # @article.create_activity :commented_on # @article.activities.last.key # => "article.commented_on" # # For other parameters, see {Tracked#activity}, and "Instance options" # accessors at {Tracked}, information on hooks is available at # {Tracked::ClassMethods#tracked}. # @see #prepare_settings # @return [Model, nil] If created successfully, new activity # @since 0.4.0 # @api public # @overload create_activity(action, options = {}) # @param [Symbol,String] action Name of the action # @param [Hash] options Options with quality higher than instance options # set in {Tracked#activity} # @option options [Activist] :owner Owner # @option options [Activist] :recipient Recipient # @option options [Hash] :params Parameters, see # {PublicActivity.resolve_value} # @overload create_activity(options = {}) # @param [Hash] options Options with quality higher than instance options # set in {Tracked#activity} # @option options [Symbol,String] :action Name of the action # @option options [String] :key Full key # @option options [Activist] :owner Owner # @option options [Activist] :recipient Recipient # @option options [Hash] :params Parameters, see # {PublicActivity.resolve_value} def create_activity(*args) return unless public_activity_enabled? options = prepare_settings(*args) if call_hook_safe(options[:key].split('.').last) reset_activity_instance_options return PublicActivity::Adapter.create_activity(self, options) end nil end # Directly saves activity to database. Works the same as create_activity # but throws validation error for each supported ORM. # # @see #create_activity def create_activity!(*args) return unless public_activity_enabled? options = prepare_settings(*args) if call_hook_safe(options[:key].split('.').last) reset_activity_instance_options PublicActivity::Adapter.create_activity!(self, options) end end # Prepares settings used during creation of Activity record. # params passed directly to tracked model have priority over # settings specified in tracked() method # # @see #create_activity # @return [Hash] Settings with preserved options that were passed # @api private # @overload prepare_settings(action, options = {}) # @see #create_activity # @overload prepare_settings(options = {}) # @see #create_activity def prepare_settings(*args) raw_options = args.extract_options! action = [args.first, raw_options.delete(:action)].compact.first key = prepare_key(action, raw_options) raise NoKeyProvided, "No key provided for #{self.class.name}" unless key prepare_custom_fields(raw_options.except(:params)).merge( { key: key, owner: prepare_relation(:owner, raw_options), recipient: prepare_relation(:recipient, raw_options), parameters: prepare_parameters(raw_options), } ) end # Prepares and resolves custom fields # users can pass to `tracked` method # @private def prepare_custom_fields(options) customs = self.class.activity_custom_fields_global.clone customs.merge!(activity_custom_fields) if activity_custom_fields customs.merge!(options) customs.each do |k, v| customs[k] = PublicActivity.resolve_value(self, v) end end # Prepares i18n parameters that will # be serialized into the Activity#parameters column # @private def prepare_parameters(options) params = {} params.merge!(self.class.activity_params_global) params.merge!(activity_params) if activity_params params.merge!([options.delete(:parameters), options.delete(:params), {}].compact.first) params.each { |k, v| params[k] = PublicActivity.resolve_value(self, v) } end # Prepares relation to be saved # to Activity. Can be :recipient or :owner # @private def prepare_relation(name, options) PublicActivity.resolve_value(self, (options.key?(name) ? options[name] : ( self.send("activity_#{name}") || self.class.send("activity_#{name}_global") ) ) ) end # Helper method to serialize class name into relevant key # @return [String] the resulted key # @param [Symbol] or [String] the name of the operation to be done on class # @param [Hash] options to be used on key generation, defaults to {} def prepare_key(action, options = {}) ( options[:key] || activity_key || ((self.class.name.underscore.gsub('/', '_') + "." + action.to_s) if action) ).try(:to_s) end # Resets all instance options on the object # triggered by a successful #create_activity, should not be # called from any other place, or from application code. # @private def reset_activity_instance_options @activity_params = {} @activity_key = nil @activity_owner = nil @activity_recipient = nil @activity_custom_fields = {} end end end ================================================ FILE: lib/public_activity/config.rb ================================================ # frozen_string_literal: true require 'singleton' module PublicActivity # Class used to initialize configuration object. class Config include ::Singleton # Evaluates given block to provide DSL configuration. # @example Initializer for Rails # PublicActivity::Config.set do # orm :mongo_mapper # enabled false # table_name "activities" # end def self.set(&block) b = Block.new b.instance_eval(&block) instance orm(b.orm) unless b.orm.nil? enabled(b.enabled) unless b.enabled.nil? table_name(b.table_name) unless b.table_name.nil? end # alias for {#orm} # @see #orm def self.orm=(orm = nil) orm(orm) end # alias for {#enabled} # @see #enabled def self.enabled=(value = nil) enabled(value) end # instance version of {Config#orm} # @see Config#orm def orm(orm = nil) self.class.orm(orm) end # instance version of {Config#table_name} # @see Config#orm def table_name(name = nil) self.class.table_name(name) end # instance version of {Config#enabled} # @see Config#orm def enabled(value = nil) self.class.enabled(value) end # Set the ORM for use by PublicActivity. def self.orm(orm = nil) if orm.nil? Thread.current[:public_activity_orm] || :active_record else Thread.current[:public_activity_orm] = orm.to_sym end end def self.table_name(name = nil) if name.nil? Thread.current[:public_activity_table_name] || "activities" else Thread.current[:public_activity_table_name] = name end end def self.enabled(value = nil) if value.nil? val = Thread.current[:public_activity_enabled] val.nil? ? true : val else Thread.current[:public_activity_enabled] = value end end # Provides simple DSL for the config block. class Block # @see Config#orm def orm(orm = nil) @orm = (orm ? orm.to_sym : false) || @orm end # Decides whether to enable PublicActivity. # @param en [Boolean] Enabled? def enabled(value = nil) @enabled = (value.nil? ? @enabled : value) end # Sets the table_name for the model def table_name(name = nil) @table_name = (name.nil? ? @table_name : name) end end end end ================================================ FILE: lib/public_activity/models/activist.rb ================================================ # frozen_string_literal: true module PublicActivity # Provides helper methods for selecting activities from a user. module Activist # Delegates to configured ORM. def self.included(base) base.extend PublicActivity.inherit_orm('Activist') end end end ================================================ FILE: lib/public_activity/models/activity.rb ================================================ # frozen_string_literal: true module PublicActivity class Activity < inherit_orm end end ================================================ FILE: lib/public_activity/models/adapter.rb ================================================ # frozen_string_literal: true module PublicActivity # Loads database-specific routines for use by PublicActivity. class Adapter < inherit_orm('Adapter') end end ================================================ FILE: lib/public_activity/models/trackable.rb ================================================ # frozen_string_literal: true module PublicActivity # Provides association for activities bound to this object by *trackable*. module Trackable # Delegates to ORM. def self.included(base) base.extend PublicActivity.inherit_orm('Trackable') end end end ================================================ FILE: lib/public_activity/orm/active_record/activist.rb ================================================ # frozen_string_literal: true module PublicActivity module ORM module ActiveRecord # Module extending classes that serve as owners module Activist # Adds ActiveRecord associations to model to simplify fetching # so you can list activities performed by the owner. # It is completely optional. Any model can be an owner to an activity # even without being an explicit activist. # # == Usage: # In model: # # class User < ActiveRecord::Base # include PublicActivity::Model # activist # end # # In controller: # User.first.activities_as_owner # User.first.activities_as_recipient # def activist has_many :activities_as_owner, class_name: '::PublicActivity::Activity', as: :owner has_many :activities_as_recipient, class_name: '::PublicActivity::Activity', as: :recipient end end end end end ================================================ FILE: lib/public_activity/orm/active_record/activity.rb ================================================ # frozen_string_literal: true module PublicActivity unless defined? ::PG::ConnectionBad module ::PG class ConnectionBad < Exception; end end end unless defined? Mysql2::Error::ConnectionError module Mysql2 module Error class ConnectionError < Exception; end end end end module ORM module ActiveRecord # The ActiveRecord model containing # details about recorded activity. class Activity < ::ActiveRecord::Base include Renderable self.table_name = PublicActivity.config.table_name self.abstract_class = true # Define polymorphic association to the parent belongs_to :trackable, polymorphic: true with_options(optional: true) do # Define ownership to a resource responsible for this activity belongs_to :owner, polymorphic: true # Define ownership to a resource targeted by this activity belongs_to :recipient, polymorphic: true end # Serialize parameters Hash begin if table_exists? unless %i[json jsonb hstore].include?(columns_hash['parameters'].type) if ::ActiveRecord.version.release < Gem::Version.new('7.1') serialize :parameters, Hash else serialize :parameters, coder: YAML, type: Hash end end else warn("[WARN] table #{name} doesn't exist. Skipping PublicActivity::Activity#parameters's serialization") end rescue ::ActiveRecord::NoDatabaseError warn("[WARN] database doesn't exist. Skipping PublicActivity::Activity#parameters's serialization") rescue ::ActiveRecord::ConnectionNotEstablished, ::PG::ConnectionBad, Mysql2::Error::ConnectionError warn("[WARN] couldn't connect to database. Skipping PublicActivity::Activity#parameters's serialization") end end end end end ================================================ FILE: lib/public_activity/orm/active_record/adapter.rb ================================================ # frozen_string_literal: true module PublicActivity module ORM # Support for ActiveRecord for PublicActivity. Used by default and supported # officialy. module ActiveRecord # Provides ActiveRecord specific, database-related routines for use by # PublicActivity. class Adapter # Creates the activity on `trackable` with `options` def self.create_activity(trackable, options) trackable.activities.create options end # Creates activity on `trackable` with `options`; throws error on validation failure def self.create_activity!(trackable, options) trackable.activities.create! options end end end end end ================================================ FILE: lib/public_activity/orm/active_record/trackable.rb ================================================ # frozen_string_literal: true module PublicActivity module ORM module ActiveRecord # Implements {PublicActivity::Trackable} for ActiveRecord # @see PublicActivity::Trackable module Trackable # Creates an association for activities where self is the *trackable* # object. def self.extended(base) base.has_many :activities, class_name: '::PublicActivity::Activity', as: :trackable end end end end end ================================================ FILE: lib/public_activity/orm/active_record.rb ================================================ # frozen_string_literal: true require 'active_record' require_relative 'active_record/activity' require_relative 'active_record/adapter' require_relative 'active_record/activist' require_relative 'active_record/trackable' ================================================ FILE: lib/public_activity/orm/mongo_mapper/activist.rb ================================================ # frozen_string_literal: true module PublicActivity module ORM module MongoMapper # Module extending classes that serve as owners module Activist # Adds MongoMapper associations to model to simplify fetching # so you can list activities performed by the owner. # It is completely optional. Any model can be an owner to an activity # even without being an explicit activist. # # == Usage: # In model: # # class User # include MongoMapper::Document # include PublicActivity::Model # activist # end # # In controller: # User.first.activities # def activist many :activities_as_owner, :class_name => "::PublicActivity::Activity", :as => :owner many :activities_as_recipient, :class_name => "::PublicActivity::Activity", :as => :recipient end end end end end ================================================ FILE: lib/public_activity/orm/mongo_mapper/activity.rb ================================================ # frozen_string_literal: true require 'mongo_mapper' require 'active_support/core_ext' module PublicActivity module ORM module MongoMapper # The MongoMapper document containing # details about recorded activity. class Activity include ::MongoMapper::Document include Renderable class SymbolHash < Hash def self.from_mongo(value) value.symbolize_keys unless value.nil? end end # Define polymorphic association to the parent belongs_to :trackable, polymorphic: true # Define ownership to a resource responsible for this activity belongs_to :owner, polymorphic: true # Define ownership to a resource targeted by this activity belongs_to :recipient, polymorphic: true key :key, String key :parameters, SymbolHash timestamps! end end end end ================================================ FILE: lib/public_activity/orm/mongo_mapper/adapter.rb ================================================ # frozen_string_literal: true module PublicActivity module ORM module MongoMapper class Adapter # Creates the activity on `trackable` with `options` def self.create_activity(trackable, options) trackable.activities.create options end # Creates activity on `trackable` with `options`; throws error on validation failure def self.create_activity!(trackable, options) trackable.activities.create! options end end end end end ================================================ FILE: lib/public_activity/orm/mongo_mapper/trackable.rb ================================================ # frozen_string_literal: true module PublicActivity module ORM module MongoMapper module Trackable def self.extended(base) base.many :activities, :class_name => "::PublicActivity::Activity", order: :created_at.asc, :as => :trackable end end end end end ================================================ FILE: lib/public_activity/orm/mongo_mapper.rb ================================================ # frozen_string_literal: true require_relative "mongo_mapper/activity.rb" require_relative "mongo_mapper/adapter.rb" require_relative "mongo_mapper/activist.rb" require_relative "mongo_mapper/trackable.rb" ================================================ FILE: lib/public_activity/orm/mongoid/activist.rb ================================================ # frozen_string_literal: true module PublicActivity module ORM module Mongoid # Module extending classes that serve as owners module Activist # Adds ActiveRecord associations to model to simplify fetching # so you can list activities performed by the owner. # It is completely optional. Any model can be an owner to an activity # even without being an explicit activist. # # == Usage: # In model: # # class User < ActiveRecord::Base # include PublicActivity::Model # activist # end # # In controller: # User.first.activities # def activist has_many :activities_as_owner, :class_name => "::PublicActivity::Activity", :inverse_of => :owner has_many :activities_as_recipient, :class_name => "::PublicActivity::Activity", :inverse_of => :recipient end end end end end ================================================ FILE: lib/public_activity/orm/mongoid/activity.rb ================================================ # frozen_string_literal: true require 'mongoid' module PublicActivity module ORM module Mongoid # The ActiveRecord model containing # details about recorded activity. class Activity include ::Mongoid::Document include ::Mongoid::Timestamps include ::Mongoid::Attributes::Dynamic if ::Mongoid::VERSION.split('.')[0].to_i >= 4 include Renderable if ::Mongoid::VERSION.split('.')[0].to_i >= 7 opts = { polymorphic: true, optional: false } else opts = { polymorphic: true } end # Define polymorphic association to the parent belongs_to :trackable, opts # Define ownership to a resource responsible for this activity belongs_to :owner, opts # Define ownership to a resource targeted by this activity belongs_to :recipient, opts field :key, type: String field :parameters, type: Hash end end end end ================================================ FILE: lib/public_activity/orm/mongoid/adapter.rb ================================================ # frozen_string_literal: true module PublicActivity module ORM module Mongoid class Adapter # Creates the activity on `trackable` with `options` def self.create_activity(trackable, options) trackable.activities.create options end # Creates activity on `trackable` with `options`; throws error on validation failure def self.create_activity!(trackable, options) trackable.activities.create! options end end end end end ================================================ FILE: lib/public_activity/orm/mongoid/trackable.rb ================================================ # frozen_string_literal: true module PublicActivity module ORM module Mongoid module Trackable def self.extended(base) base.has_many :activities, :class_name => "::PublicActivity::Activity", :as => :trackable end end end end end ================================================ FILE: lib/public_activity/orm/mongoid.rb ================================================ # frozen_string_literal: true require_relative "mongoid/activity.rb" require_relative "mongoid/adapter.rb" require_relative "mongoid/activist.rb" require_relative "mongoid/trackable.rb" ================================================ FILE: lib/public_activity/renderable.rb ================================================ # frozen_string_literal: true module PublicActivity # Provides logic for rendering activities. Handles both i18n strings # support and smart partials rendering (different templates per activity key). module Renderable # Virtual attribute returning text description of the activity # using the activity's key to translate using i18n. def text(params = {}) # TODO: some helper for key transformation for two supported formats k = key.split('.') k.unshift('activity') if k.first != 'activity' k = k.join('.') translate_params = parameters.merge(params) || {} I18n.t(k, **translate_params) end # Renders activity from views. # # @param [ActionView::Base] context # @return [nil] nil # # Renders activity to the given ActionView context with included # AV::Helpers::RenderingHelper (most commonly just ActionView::Base) # # The *preferred* *way* of rendering activities is # to provide a template specifying how the rendering should be happening. # However, one may choose using _I18n_ based approach when developing # an application that supports plenty of languages. # # If partial view exists that matches the *key* attribute # renders that partial with local variables set to contain both # Activity and activity_parameters (hash with indifferent access) # # Otherwise, it outputs the I18n translation to the context # @example Render a list of all activities from a view (erb) #
    # <% for activity in PublicActivity::Activity.all %> #
  • <%= render_activity(activity) %>
  • # <% end %> #
# # = Layouts # You can supply a layout that will be used for activity partials # with :layout param. # Keep in mind that layouts for partials are also partials. # @example Supply a layout # # in views: # # All examples look for a layout in app/views/layouts/_activity.erb # render_activity @activity, :layout => "activity" # render_activity @activity, :layout => "layouts/activity" # render_activity @activity, :layout => :activity # # # app/views/layouts/_activity.erb #

<%= a.created_at %>

# <%= yield %> # # == Custom Layout Location # You can customize the layout directory by supplying :layout_root # or by using an absolute path. # # @example Declare custom layout location # # # Both examples look for a layout in "app/views/custom/_layout.erb" # # render_activity @activity, :layout_root => "custom" # render_activity @activity, :layout => "/custom/layout" # # = Creating a template # To use templates for formatting how the activity should render, # create a template based on activity key, for example: # # Given a key _activity.article.create_, create directory tree # _app/views/public_activity/article/_ and create the _create_ partial there # # Note that if a key consists of more than three parts splitted by commas, your # directory structure will have to be deeper, for example: # activity.article.comments.destroy => app/views/public_activity/articles/comments/_destroy.html.erb # # == Custom Directory # You can override the default `public_directory` template root with the :root parameter # # @example Custom template root # # look for templates inside of /app/views/custom instead of /app/views/public_directory # render_activity @activity, :root => "custom" # # == Variables in templates # From within a template there are two variables at your disposal: # * activity (aliased as *a* for a shortcut) # * params (aliased as *p*) [converted into a HashWithIndifferentAccess] # # @example Template for key: _activity.article.create_ (erb) #

# Article <%= p[:name] %> # was written by <%= p["author"] %> # <%= distance_of_time_in_words_to_now(a.created_at) %> #

def render(context, params = {}) partial_root = params.delete(:root) || 'public_activity' partial_path = nil layout_root = params.delete(:layout_root) || 'layouts' if params.has_key? :display if params[:display].to_sym == :"i18n" text = self.text(params) return context.render :text => text, :plain => text else partial_path = File.join(partial_root, params[:display].to_s) end end context.render( params.merge({ :partial => prepare_partial(partial_root, partial_path), :layout => prepare_layout(layout_root, params.delete(:layout)), :locals => prepare_locals(params) }) ) end def prepare_partial(root, path) path || self.template_path(self.key, root) end def prepare_locals(params) locals = params.delete(:locals) || Hash.new controller = PublicActivity.get_controller prepared_params = prepare_parameters(params) locals.merge( { :a => self, :activity => self, :controller => controller, :current_user => controller.respond_to?(:current_user) ? controller.current_user : nil, :p => prepared_params, :params => prepared_params } ) end def prepare_layout(root, layout) if layout path = layout.to_s unless path.start_with?(root) || path.start_with?("/") return File.join(root, path) end end layout end def prepare_parameters(params) if self.parameters @prepared_params ||= self.parameters.with_indifferent_access.merge(params) else @prepared_params ||= params end end protected # Builds the path to template based on activity key def template_path(key, partial_root) path = key.split(".") path.delete_at(0) if path[0] == "activity" path.unshift partial_root path.join("/") end end end ================================================ FILE: lib/public_activity/roles/deactivatable.rb ================================================ # frozen_string_literal: true module PublicActivity # Enables per-class disabling of PublicActivity functionality. module Deactivatable extend ActiveSupport::Concern included do class_attribute :public_activity_enabled_for_model set_public_activity_class_defaults end # Returns true if PublicActivity is enabled # globally and for this class. # @return [Boolean] # @api private # @since 0.5.0 # overrides the method from Common def public_activity_enabled? PublicActivity.enabled? && self.class.public_activity_enabled_for_model end # Provides global methods to disable or enable PublicActivity on a per-class # basis. module ClassMethods # Switches public_activity off for this class def public_activity_off self.public_activity_enabled_for_model = false end # Switches public_activity on for this class def public_activity_on self.public_activity_enabled_for_model = true end # @since 1.0.0 # @api private def set_public_activity_class_defaults super self.public_activity_enabled_for_model = true end end end end ================================================ FILE: lib/public_activity/roles/tracked.rb ================================================ # frozen_string_literal: true module PublicActivity # Main module extending classes we want to keep track of. module Tracked extend ActiveSupport::Concern # A shortcut method for setting custom key, owner and parameters of {Activity} # in one line. Accepts a hash with 3 keys: # :key, :owner, :params. You can specify all of them or just the ones you want to overwrite. # # == Options # # [:key] # See {Common#activity_key} # [:owner] # See {Common#activity_owner} # [:params] # See {Common#activity_params} # [:recipient] # Set the recipient for this activity. Useful for private notifications, which should only be visible to a certain user. See {Common#activity_recipient}. # @example # # @article = Article.new # @article.title = "New article" # @article.activity :key => "my.custom.article.key", :owner => @article.author, :params => {:title => @article.title} # @article.save # @article.activities.last.key #=> "my.custom.article.key" # @article.activities.last.parameters #=> {:title => "New article"} # # @param options [Hash] instance options to set on the tracked model # @return [nil] def activity(options = {}) rest = options.clone self.activity_key = rest.delete(:key) if rest[:key] self.activity_owner = rest.delete(:owner) if rest[:owner] self.activity_params = rest.delete(:params) if rest[:params] self.activity_recipient = rest.delete(:recipient) if rest[:recipient] self.activity_custom_fields = rest if rest.count > 0 nil end # Module with basic +tracked+ method that enables tracking models. class_methods do # Adds required callbacks for creating and updating # tracked models and adds +activities+ relation for listing # associated activities. # # == Parameters: # [:owner] # Specify the owner of the {Activity} (person responsible for the action). # It can be a Proc, Symbol or an ActiveRecord object: # == Examples: # # tracked :owner => :author # tracked :owner => proc {|o| o.author} # # Keep in mind that owner relation is polymorphic, so you can't just # provide id number of the owner object. # [:recipient] # Specify the recipient of the {Activity} # It can be a Proc, Symbol, or an ActiveRecord object # == Examples: # # tracked :recipient => :author # tracked :recipient => proc {|o| o.author} # # Keep in mind that recipient relation is polymorphic, so you can't just # provide id number of the owner object. # [:params] # Accepts a Hash with custom parameters you want to pass to i18n.translate # method. It is later used in {Renderable#text} method. # == Example: # class Article < ActiveRecord::Base # include PublicActivity::Model # tracked :params => { # :title => :title, # :author_name => "Michael", # :category_name => proc {|controller, model_instance| model_instance.category.name}, # :summary => proc {|controller, model_instance| truncate(model.text, :length => 30)} # } # end # # Values in the :params hash can either be an *exact* *value*, a *Proc/Lambda* executed before saving the activity or a *Symbol* # which is a an attribute or a method name executed on the tracked model's instance. # # Everything specified here has a lower priority than parameters # specified directly in {#activity} method. # So treat it as a place where you provide 'default' values or where you # specify what data should be gathered for every activity. # For more dynamic settings refer to {Activity} model documentation. # [:skip_defaults] # Disables recording of activities on create/update/destroy leaving that to programmer's choice. Check {PublicActivity::Common#create_activity} # for a guide on how to manually record activities. # [:only] # Accepts a symbol or an array of symbols, of which any combination of the three is accepted: # * _:create_ # * _:update_ # * _:destroy_ # Selecting one or more of these will make PublicActivity create activities # automatically for the tracked model on selected actions. # # Resulting activities will have have keys assigned to, respectively: # * _article.create_ # * _article.update_ # * _article.destroy_ # Since only three options are valid, # see _:except_ option for a shorter version # [:except] # Accepts a symbol or an array of symbols with values like in _:only_, above. # Values provided will be subtracted from all default actions: # (create, update, destroy). # # So, passing _create_ would track and automatically create # activities on _update_ and _destroy_ actions, # but not on the _create_ action. # [:on] # Accepts a Hash with key being the *action* on which to execute *value* (proc) # Currently supported only for CRUD actions which are enabled in _:only_ # or _:except_ options on this method. # # Key-value pairs in this option define callbacks that can decide # whether to create an activity or not. Procs have two attributes for # use: _model_ and _controller_. If the proc returns true, the activity # will be created, if not, then activity will not be saved. # # == Example: # # app/models/article.rb # tracked :on => {:update => proc {|model, controller| model.published? }} # # In the example above, given a model Article with boolean column _published_. # The activities with key _article.update_ will only be created # if the published status is set to true on that article. # @param opts [Hash] options # @return [nil] options def tracked(opts = {}) options = opts.clone include_default_actions(options) assign_globals options assign_hooks options assign_custom_fields options nil end def include_default_actions(options) defaults = { create: Creation, destroy: Destruction, update: Update } return if options[:skip_defaults] == true modules = if options[:except] defaults.except(*options[:except]) elsif options[:only] defaults.slice(*options[:only]) else defaults end modules.each_value { |mod| include mod } end def available_options %i[skip_defaults only except on owner recipient params].freeze end def assign_globals(options) %i[owner recipient params].each do |key| next unless options[key] send("activity_#{key}_global=".to_sym, options.delete(key)) end end def assign_hooks(options) return unless options[:on].is_a?(Hash) self.activity_hooks = options[:on].select { |_, v| v.is_a? Proc }.symbolize_keys end def assign_custom_fields(options) options.except(*available_options).each do |k, v| activity_custom_fields_global[k] = v end end end end end ================================================ FILE: lib/public_activity/testing.rb ================================================ # frozen_string_literal: true # This file provides functionality for testing your code with public_activity # activated or deactivated. # This file should only be required in test/spec code! # # To enable PublicActivity testing capabilities do: # require 'public_activity/testing' module PublicActivity # Execute the code block with PublicActiviy active # # Example usage: # PublicActivity.with_tracking do # # your test code here # end def self.with_tracking current = PublicActivity.config.enabled PublicActivity.config.enabled(true) yield ensure PublicActivity.config.enabled(current) end # Execute the code block with PublicActiviy deactive # # Example usage: # PublicActivity.without_tracking do # # your test code here # end def self.without_tracking current = PublicActivity.enabled? PublicActivity.config.enabled(false) yield ensure PublicActivity.config.enabled(current) end end ================================================ FILE: lib/public_activity/utility/store_controller.rb ================================================ # frozen_string_literal: true module PublicActivity class << self # Setter for remembering controller instance def set_controller(controller) Thread.current[:public_activity_controller] = controller end # Getter for accessing the controller instance def get_controller Thread.current[:public_activity_controller] end end # Module included in controllers to allow p_a access to controller instance module StoreController extend ActiveSupport::Concern included do around_action :store_controller_for_public_activity if respond_to?(:around_action) around_filter :store_controller_for_public_activity unless respond_to?(:around_action) end def store_controller_for_public_activity PublicActivity.set_controller(self) yield ensure PublicActivity.set_controller(nil) end end end ================================================ FILE: lib/public_activity/utility/view_helpers.rb ================================================ # frozen_string_literal: true # Provides a shortcut from views to the rendering method. module PublicActivity # Module extending ActionView::Base and adding `render_activity` helper. module ViewHelpers # View helper for rendering an activity, calls {PublicActivity::Activity#render} internally. def render_activity activities, options = {} if activities.is_a? PublicActivity::Activity activities.render self, options elsif activities.respond_to?(:map) # depend on ORMs to fetch as needed # maybe we can support Postgres streaming with this? activities.map {|activity| activity.render self, options.dup }.join.html_safe end end alias_method :render_activities, :render_activity # Helper for setting content_for in activity partial, needed to # flush remains in between partial renders. def single_content_for(name, content = nil, &block) @view_flow.set(name, ActiveSupport::SafeBuffer.new) content_for(name, content, &block) end end end ActiveSupport.on_load(:action_view) do include PublicActivity::ViewHelpers end ================================================ FILE: lib/public_activity/version.rb ================================================ # frozen_string_literal: true module PublicActivity VERSION = '3.0.2' end ================================================ FILE: lib/public_activity.rb ================================================ # frozen_string_literal: true require 'active_support' require 'logger' require 'action_view' # +public_activity+ keeps track of changes made to models # and allows you to display them to the users. # # Check {PublicActivity::Tracked::ClassMethods#tracked} for more details about customizing and specifying # ownership to users. module PublicActivity extend ActiveSupport::Concern extend ActiveSupport::Autoload autoload :Activity, 'public_activity/models/activity' autoload :Activist, 'public_activity/models/activist' autoload :Adapter, 'public_activity/models/adapter' autoload :Trackable, 'public_activity/models/trackable' autoload :Common autoload :Config autoload :Creation, 'public_activity/actions/creation.rb' autoload :Deactivatable,'public_activity/roles/deactivatable.rb' autoload :Destruction, 'public_activity/actions/destruction.rb' autoload :Renderable autoload :Tracked, 'public_activity/roles/tracked.rb' autoload :Update, 'public_activity/actions/update.rb' autoload :VERSION # Switches PublicActivity on or off. # @param value [Boolean] # @since 0.5.0 def self.enabled=(value) config.enabled(value) end # Returns `true` if PublicActivity is on, `false` otherwise. # Enabled by default. # @return [Boolean] # @since 0.5.0 def self.enabled? config.enabled end # Returns PublicActivity's configuration object. # @since 0.5.0 def self.config @@config ||= PublicActivity::Config.instance end # Method used to choose which ORM to load # when PublicActivity::Activity class is being autoloaded def self.inherit_orm(model = 'Activity') orm = PublicActivity.config.orm require "public_activity/orm/#{orm}" "PublicActivity::ORM::#{orm.to_s.classify}::#{model}".constantize end # Module to be included in ActiveRecord models. Adds required functionality. module Model extend ActiveSupport::Concern included do include Common include Deactivatable include Tracked include Activist # optional associations by recipient|owner end end end require 'public_activity/utility/store_controller' require 'public_activity/utility/view_helpers' ================================================ FILE: public_activity.gemspec ================================================ # frozen_string_literal: true $LOAD_PATH.push File.expand_path('../lib', __FILE__) require 'public_activity/version' Gem::Specification.new do |s| s.name = 'public_activity' s.version = PublicActivity::VERSION s.platform = Gem::Platform::RUBY s.authors = ['Juri Hahn', 'Piotrek Okoński', 'Kuba Okoński'] s.email = 'juri.hahn+public-activity@gmail.com' s.homepage = 'https://github.com/public-activity/public_activity' s.summary = 'Easy activity tracking for ActiveRecord models' s.description = 'Easy activity tracking for your ActiveRecord models. Provides Activity model with details about actions performed by your users, like adding comments, responding etc.' s.license = 'MIT' s.metadata = { "bug_tracker_uri" => "https://github.com/public-activity/public_activity/issues", 'changelog_uri' => 'https://github.com/public-activity/public_activity/blob/main/CHANGELOG.md', "documentation_uri" => "https://rubydoc.info/gems/public_activity", "homepage_uri" => s.homepage, "source_code_uri" => "https://github.com/public-activity/public_activity", "rubygems_mfa_required" => "true", } s.files = `git ls-files lib`.split("\n") + ['Gemfile', 'Rakefile', 'README.md', 'MIT-LICENSE'] s.test_files = `git ls-files test`.split("\n") s.require_paths = ['lib'] s.required_ruby_version = '>= 3.0.0' s.post_install_message = File.read('UPGRADING') if File.exist?('UPGRADING') s.add_dependency 'actionpack', '>= 6.1' s.add_dependency 'i18n', '>= 0.5.0' s.add_dependency 'railties', '>= 6.1' ENV['PA_ORM'] ||= 'active_record' case ENV['PA_ORM'] when 'active_record' s.add_dependency 'activerecord', '>= 6.1' when 'mongoid' s.add_dependency 'mongoid', '>= 4.0' when 'mongo_mapper' s.add_dependency 'bson_ext' s.add_dependency 'mongo', '<= 1.9.2' s.add_dependency 'mongo_mapper', '>= 0.12.0' end s.add_development_dependency 'appraisal' s.add_development_dependency 'minitest' s.add_development_dependency 'mocha' s.add_development_dependency 'pry' s.add_development_dependency 'redcarpet' s.add_development_dependency 'simplecov' s.add_development_dependency 'sqlite3', '~> 1.4' s.add_development_dependency 'test-unit' s.add_development_dependency 'yard' s.add_development_dependency 'rake' end ================================================ FILE: public_activity.sublime-project ================================================ { "folders": [ { "path": "./", "folder_exclude_patterns": ["coverage", ".yardoc", "doc"] } ], "settings": { "tab_size": 2, "translate_tabs_to_spaces": true, "trim_trailing_white_space_on_save": true }, "build_systems": [ { "name": "PublicActivity Test Suite", "linux": { "working_dir": "$project_dir", "cmd": ["bundle", "exec", "rake", "test"] }, "selector": "source.ruby" } ] } ================================================ FILE: test/migrations/002_create_articles.rb ================================================ # frozen_string_literal: true class CreateArticles < ActiveRecord::Migration[6.1] def self.up create_table :articles do |t| t.string :name t.boolean :published t.belongs_to :user t.timestamps end end end ================================================ FILE: test/migrations/003_create_users.rb ================================================ # frozen_string_literal: true class CreateUsers < ActiveRecord::Migration[6.1] def self.up create_table :users do |t| t.string :name t.timestamps end end end ================================================ FILE: test/migrations/004_add_nonstandard_to_activities.rb ================================================ # frozen_string_literal: true class AddNonstandardToActivities < ActiveRecord::Migration[6.1] def change change_table :activities do |t| t.string :nonstandard end end end ================================================ FILE: test/mongo_mapper.yml ================================================ test: host: 127.0.0.1 port: 27017 database: public_activity_test ================================================ FILE: test/mongoid.yml ================================================ test: clients: default: hosts: - 127.0.0.1:27017 database: public_activity_test ================================================ FILE: test/test_activist.rb ================================================ # frozen_string_literal: true require 'test_helper' describe PublicActivity::Activist do it 'adds owner association' do klass = article assert_respond_to klass, :activist klass.activist assert_respond_to klass.new, :activities case ENV['PA_ORM'] when 'active_record' assert_equal klass.reflect_on_association(:activities_as_owner).options[:as], :owner when 'mongoid' assert_equal klass.reflect_on_association(:activities_as_owner).options[:inverse_of], :owner when 'mongo_mapper' assert_equal klass.associations[:activities_as_owner].options[:as], :owner end if ENV['PA_ORM'] == 'mongo_mapper' assert_equal klass.associations[:activities_as_owner].options[:class_name], '::PublicActivity::Activity' else assert_equal klass.reflect_on_association(:activities_as_owner).options[:class_name], '::PublicActivity::Activity' end end it 'returns activities from association' do case PublicActivity::Config.orm when :active_record class ActivistUser < ActiveRecord::Base include PublicActivity::Model self.table_name = 'users' activist end when :mongoid class ActivistUser include Mongoid::Document include PublicActivity::Model activist field :name, type: String end when :mongo_mapper class ActivistUser include MongoMapper::Document include PublicActivity::Model activist key :name, String end end owner = ActivistUser.create(name: 'Peter Pan') a = article(owner: owner).new a.save assert_equal owner.activities_as_owner.length, 1 end end ================================================ FILE: test/test_activity.rb ================================================ # frozen_string_literal: true require 'test_helper' describe 'PublicActivity::Activity Rendering' do describe '#text' do subject { PublicActivity::Activity.new(key: 'activity.test', parameters: { one: 1 }) } specify '#text uses translations' do subject.save I18n.config.backend.store_translations(:en, activity: { test: '%{one} %{two}' }) assert_equal subject.text(two: 2), '1 2' assert_equal subject.parameters, one: 1 end end describe '#render' do subject do PublicActivity::Activity.new(key: 'activity.test', parameters: { one: 1 }).tap(&:save) end let(:template_output) { "1, 2\nactivity.test, #{subject.id}\n" } before { @controller.class.prepend_view_path File.expand_path('views', __dir__) } it 'uses view partials when available' do PublicActivity.set_controller(Struct.new(:current_user).new('fake')) subject.render(self, two: 2) assert_equal rendered, "#{template_output}fake\n" end it 'uses requested partial' it 'uses view partials without controller' do PublicActivity.set_controller(nil) subject.render(self, two: 2) assert_equal rendered, "#{template_output}\n" end it 'provides local variables' do PublicActivity.set_controller(nil) subject.render(self, locals: { two: 2 }) assert_equal rendered.chomp, '2' end it 'uses translations only when requested' do I18n.config.backend.store_translations(:en, activity: { test: '%{one} %{two}' }) subject.render(self, two: 2, display: :i18n) assert_equal rendered, '1 2' end it 'pass all params to view context' do view_context = mock('ViewContext') PublicActivity.set_controller(nil) view_context.expects(:render).with { |params| params[:formats] == ['json'] } subject.render(view_context, formats: ['json']) end it 'uses specified layout' do PublicActivity.set_controller(nil) subject.render(self, layout: 'activity') assert_includes rendered, 'Here be the layouts' subject.render(self, layout: 'layouts/activity') assert_includes rendered, 'Here be the layouts' subject.render(self, layout: :activity) assert_includes rendered, 'Here be the layouts' end it 'accepts a custom layout root' do subject.render(self, layout: :layout, layout_root: 'custom') assert_includes rendered, 'Here be the custom layouts' end it 'accepts an absolute layout path' do subject.render(self, layout: '/custom/layout') assert_includes rendered, 'Here be the custom layouts' end it 'accepts a template root' do subject.render(self, root: 'custom') assert_includes rendered, 'Custom Template Root' end end end ================================================ FILE: test/test_common.rb ================================================ # frozen_string_literal: true require 'test_helper' describe PublicActivity::Common do before do @owner = User.create(name: 'Peter Pan') @recipient = User.create(name: 'Bruce Wayne') @options = { params: { author_name: 'Peter', summary: 'Default summary goes here...' }, owner: @owner, recipient: @recipient } end subject { article(@options).new } it 'prioritizes parameters passed to #create_activity' do subject.save assert_equal subject.create_activity(:test, params: { author_name: 'Pan' }).parameters[:author_name], 'Pan' assert_equal subject.create_activity(:test, parameters: { author_name: 'Pan' }).parameters[:author_name], 'Pan' assert_nil subject.create_activity(:test, params: { author_name: nil }).parameters[:author_name] assert_nil subject.create_activity(:test, parameters: { author_name: nil }).parameters[:author_name] end it 'prioritizes owner passed to #create_activity' do subject.save assert_equal subject.create_activity(:test, owner: @recipient).owner, @recipient assert_nil subject.create_activity(:test, owner: nil).owner end it 'prioritizes recipient passed to #create_activity' do subject.save assert_equal subject.create_activity(:test, recipient: @owner).recipient, @owner assert_nil subject.create_activity(:test, recipient: nil).recipient end it 'uses global fields' do subject.save activity = subject.activities.last assert_equal activity.parameters, @options[:params] assert_equal activity.owner, @owner end it 'allows custom fields' do subject.save subject.create_activity :with_custom_fields, nonstandard: 'Custom allowed' assert_equal subject.activities.last.nonstandard, 'Custom allowed' end it '#create_activity returns a new activity object' do subject.save assert subject.create_activity('some.key') end it '#create_activity! returns a new activity object' do subject.save activity = subject.create_activity!('some.key') assert activity.persisted? assert_equal 'article.some.key', activity.key end it 'update action should not create activity on save unless model changed' do subject.save before_count = subject.activities.count subject.save subject.save after_count = subject.activities.count assert_equal before_count, after_count end it 'allows passing owner through #create_activity' do article = article().new article.save activity = article.create_activity('some.key', owner: @owner) assert_equal activity.owner, @owner end it 'allows resolving custom fields' do subject.name = 'Resolving is great' subject.published = true subject.save subject.create_activity :with_custom_fields, nonstandard: :name assert_equal subject.activities.last.nonstandard, 'Resolving is great' subject.create_activity :with_custom_fields_2, nonstandard: proc { |_, model| model.published.to_s } assert_equal subject.activities.last.nonstandard, 'true' end it 'inherits instance parameters' do subject.activity params: { author_name: 'Michael' } subject.save activity = subject.activities.last assert_equal activity.parameters[:author_name], 'Michael' end it 'accepts instance recipient' do subject.activity recipient: @recipient subject.save assert_equal subject.activities.last.recipient, @recipient end it 'accepts instance owner' do subject.activity owner: @owner subject.save assert_equal subject.activities.last.owner, @owner end it 'accepts owner as a symbol' do klass = article(owner: :user) @article = klass.new(user: @owner) @article.save activity = @article.activities.last assert_equal activity.owner, @owner end it 'reports PublicActivity::Activity as the base class' do if ENV['PA_ORM'] == 'active_record' # Only relevant for ActiveRecord subject.save assert_equal subject.activities.last.class.base_class, PublicActivity::Activity end end describe '#prepare_key' do describe 'for class#activity_key method' do before do @article = article(owner: :user).new(user: @owner) end it 'assigns key to value of activity_key if set' do def @article.activity_key; 'my_custom_key' end assert_equal @article.prepare_key(:create, {}), 'my_custom_key' end it 'assigns key based on class name as fallback' do def @article.activity_key; nil end assert_equal @article.prepare_key(:create), 'article.create' end it 'assigns key value from options hash' do assert_equal @article.prepare_key(:create, key: :my_custom_key), 'my_custom_key' end end describe 'for camel cased classes' do before do class CamelCase < article(owner: :user) def self.name; 'CamelCase' end end @camel_case = CamelCase.new end it 'assigns generates key from class name' do assert_equal @camel_case.prepare_key(:create, {}), 'camel_case.create' end end describe 'for namespaced classes' do before do module ::MyNamespace; class CamelCase < article(owner: :user) def self.name; 'MyNamespace::CamelCase' end end end @namespaced_camel_case = MyNamespace::CamelCase.new end it 'assigns key value from options hash' do assert_equal @namespaced_camel_case.prepare_key(:create, {}), 'my_namespace_camel_case.create' end end end # no key implicated or given specify do assert_raises(PublicActivity::NoKeyProvided) { subject.prepare_settings } end describe 'resolving values' do it 'allows procs with models and controllers' do context = mock('context') context.expects(:accessor).times(2).returns(5) controller = mock('controller') controller.expects(:current_user).returns(:cu) PublicActivity.set_controller(controller) p = proc { |c, m| assert_equal :cu, c.current_user assert_equal 5, m.accessor } PublicActivity.resolve_value(context, p) PublicActivity.resolve_value(context, :accessor) end end def teardown PublicActivity.set_controller(nil) end end ================================================ FILE: test/test_controller_integration.rb ================================================ # frozen_string_literal: true require 'test_helper' class StoringController < ActionView::TestCase::TestController include PublicActivity::StoreController include ActionController::Testing end describe PublicActivity::StoreController do it 'stores controller' do controller = StoringController.new PublicActivity.set_controller(controller) assert_same controller, Thread.current[:public_activity_controller] assert_same controller, PublicActivity.get_controller end it 'stores controller with a filter in controller' do controller = StoringController.new callbacks = controller._process_action_callbacks.select { |c| c.kind == :around }.map(&:filter) assert_includes(callbacks, :store_controller_for_public_activity) public_activity_controller = controller.instance_eval do store_controller_for_public_activity do PublicActivity.get_controller end end assert_equal controller, public_activity_controller end it 'stores controller in a threadsafe way' do PublicActivity.set_controller(1) assert_equal PublicActivity.get_controller, 1 Thread.new do PublicActivity.set_controller(2) assert_equal 2, PublicActivity.get_controller PublicActivity.set_controller(nil) end assert_equal PublicActivity.get_controller, 1 PublicActivity.set_controller(nil) end end ================================================ FILE: test/test_generators.rb ================================================ # frozen_string_literal: true if ENV['PA_ORM'] == 'active_record' require 'test_helper' require 'rails/generators/test_case' require 'generators/public_activity/migration/migration_generator' require 'generators/public_activity/migration_upgrade/migration_upgrade_generator' class TestMigrationGenerator < Rails::Generators::TestCase tests PublicActivity::Generators::MigrationGenerator destination File.expand_path('../tmp', File.dirname(__FILE__)) setup :prepare_destination def test_generating_activity_model run_generator assert_migration 'db/migrate/create_activities.rb' end end class TestMigrationUpgradeGenerator < Rails::Generators::TestCase tests PublicActivity::Generators::MigrationUpgradeGenerator destination File.expand_path('../tmp', File.dirname(__FILE__)) setup :prepare_destination def test_generating_activity_model run_generator assert_migration 'db/migrate/upgrade_activities.rb' end end end ================================================ FILE: test/test_helper.rb ================================================ # frozen_string_literal: true require 'rubygems' require 'bundler' require 'logger' Bundler.setup(:default, :test) if ENV['COV'] require 'simplecov' SimpleCov.start do add_filter '/test/' end end $:.unshift File.expand_path('../lib', __dir__) require 'active_support/testing/setup_and_teardown' require 'public_activity' require 'public_activity/testing' require 'pry' require 'minitest/autorun' require 'mocha/minitest' PublicActivity::Config.orm = (ENV['PA_ORM'] || :active_record) case PublicActivity::Config.orm when :active_record require 'active_record' require 'active_record/connection_adapters/sqlite3_adapter' require 'stringio' # silence the output $stdout = StringIO.new # from migrator ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') migrations_path = File.expand_path('migrations', __dir__) active_record_version = ActiveRecord.version.release if active_record_version >= Gem::Version.new('7.2.0') migration_context = ActiveRecord::MigrationContext.new(migrations_path) migrations = migration_context.migrations # => Array connection_pool = ActiveRecord::Tasks::DatabaseTasks.migration_connection_pool schema_migration = ActiveRecord::SchemaMigration.new(connection_pool) internal_metadata = ActiveRecord::InternalMetadata.new(connection_pool) ActiveRecord::Migrator.new( :up, migrations, schema_migration, internal_metadata ).migrate else ActiveRecord::MigrationContext.new(migrations_path, ActiveRecord::SchemaMigration).migrate end $stdout = STDOUT def article(options = {}) Class.new(ActiveRecord::Base) do self.table_name = 'articles' include PublicActivity::Model tracked options belongs_to :user def self.name 'Article' end end end class User < ActiveRecord::Base; end when :mongoid require 'mongoid' Mongoid.load!(File.expand_path('test/mongoid.yml'), :test) class User include Mongoid::Document include Mongoid::Timestamps has_many :articles field :name, type: String end class Article include Mongoid::Document include Mongoid::Timestamps include PublicActivity::Model if ::Mongoid::VERSION.split('.')[0].to_i >= 7 belongs_to :user, optional: true else belongs_to :user end field :name, type: String field :published, type: Boolean end def article(options = {}) Article.class_eval do set_public_activity_class_defaults tracked options end Article end when :mongo_mapper require 'mongo_mapper' config = YAML.safe_load(File.read('test/mongo_mapper.yml'), aliases: true) MongoMapper.setup(config, :test) class User include MongoMapper::Document has_many :articles key :name, String timestamps! end class Article include MongoMapper::Document include PublicActivity::Model belongs_to :user key :name, String key :published, Boolean end def article(options = {}) Article.class_eval do set_public_activity_class_defaults tracked options end Article end end class ViewSpec < Minitest::Spec prepend ActiveSupport::Testing::SetupAndTeardown include ActionView::TestCase::Behavior end Minitest::Spec.register_spec_type(/Rendering$/, ViewSpec) ================================================ FILE: test/test_testing.rb ================================================ # frozen_string_literal: true require 'test_helper' describe PublicActivity do describe 'self.with_tracking' do after do PublicActivity.enabled = true end it 'enables tracking inside the block' do PublicActivity.enabled = false PublicActivity.with_tracking do assert PublicActivity.enabled? end end it 'restores previous `enabled` state' do PublicActivity.enabled = false PublicActivity.with_tracking do # something end assert_equal PublicActivity.enabled?, false end end describe 'self.without_tracking' do it 'disables tracking inside the block' do PublicActivity.enabled = true PublicActivity.without_tracking do assert_equal PublicActivity.enabled?, false end end end end ================================================ FILE: test/test_tracking.rb ================================================ # frozen_string_literal: true require 'test_helper' describe PublicActivity::Tracked do describe 'defining instance options' do subject { article.new } let :options do { key: 'key', params: { a: 1 }, owner: User.create, recipient: User.create } end before(:each) { subject.activity(options) } let(:activity) { subject.save; subject.activities.last } specify { assert_same subject.activity_key, options[:key] } specify { assert_equal activity.key, options[:key] } specify { assert_same subject.activity_owner, options[:owner] } specify { assert_equal activity.owner, options[:owner] } specify { assert_same subject.activity_params, options[:params] } specify { assert_equal activity.parameters, options[:params] } specify { assert_same subject.activity_recipient, options[:recipient] } specify { assert_equal activity.recipient, options[:recipient] } end it 'can be tracked and be an activist at the same time' do case PublicActivity.config.orm when :mongoid class ActivistAndTrackedArticle include Mongoid::Document include Mongoid::Timestamps include PublicActivity::Model if ::Mongoid::VERSION.split('.')[0].to_i >= 7 belongs_to :user, optional: true else belongs_to :user end field :name, type: String field :published, type: Boolean tracked activist end when :mongo_mapper class ActivistAndTrackedArticle include MongoMapper::Document include PublicActivity::Model belongs_to :user key :name, String key :published, Boolean tracked activist timestamps! end when :active_record class ActivistAndTrackedArticle < ActiveRecord::Base self.table_name = 'articles' include PublicActivity::Model tracked activist belongs_to :user end end art = ActivistAndTrackedArticle.new art.save assert_equal art.activities.last.trackable_id, art.id assert_nil art.activities.last.owner_id end describe 'custom fields' do describe 'global' do it 'should resolve symbols' do a = article(nonstandard: :name).new(name: 'Symbol resolved') a.save assert_equal a.activities.last.nonstandard, 'Symbol resolved' end it 'should resolve procs' do a = article(nonstandard: proc { |_, model| model.name }).new(name: 'Proc resolved') a.save assert_equal a.activities.last.nonstandard, 'Proc resolved' end end describe 'instance' do it 'should resolve symbols' do a = article.new(name: 'Symbol resolved') a.activity nonstandard: :name a.save assert_equal a.activities.last.nonstandard, 'Symbol resolved' end it 'should resolve procs' do a = article.new(name: 'Proc resolved') a.activity nonstandard: proc { |_, model| model.name } a.save assert_equal a.activities.last.nonstandard, 'Proc resolved' end end end it 'should reset instance options on successful create_activity' do a = article.new a.activity key: 'test', params: { test: 1 } a.save assert_equal a.activities.count, 1 assert_raises(PublicActivity::NoKeyProvided) { a.create_activity } assert_empty a.activity_params a.activity key: 'asd' a.create_activity assert_raises(PublicActivity::NoKeyProvided) { a.create_activity } end it 'should not accept global key option' do # this example tests the lack of presence of sth that should not be here a = article(key: 'asd').new a.save assert_raises(PublicActivity::NoKeyProvided) { a.create_activity } assert_equal a.activities.count, 1 end it 'should not change global custom fields' do a = article(nonstandard: 'global').new a.activity nonstandard: 'instance' a.save assert_equal a.class.activity_custom_fields_global, nonstandard: 'global' end describe 'disabling functionality' do it 'allows for global disable' do PublicActivity.enabled = false activity_count_before = PublicActivity::Activity.count @article = article.new @article.save assert_equal PublicActivity::Activity.count, activity_count_before PublicActivity.enabled = true end it 'allows for class-wide disable' do activity_count_before = PublicActivity::Activity.count klass = article klass.public_activity_off @article = klass.new @article.save assert_equal PublicActivity::Activity.count, activity_count_before klass.public_activity_on @article.name = 'Changed Article' @article.save assert(PublicActivity::Activity.count > activity_count_before) end end describe '#tracked' do subject { article(options) } let(:options) { {} } it 'allows skipping the tracking on CRUD actions' do art = case PublicActivity.config.orm when :mongoid Class.new do include Mongoid::Document include Mongoid::Timestamps include PublicActivity::Model belongs_to :user field :name, type: String field :published, type: Boolean tracked skip_defaults: true end when :mongo_mapper Class.new do include MongoMapper::Document include PublicActivity::Model belongs_to :user key :name, String key :published, Boolean tracked skip_defaults: true timestamps! end when :active_record article(skip_defaults: true) end assert_includes art, PublicActivity::Common refute_includes art, PublicActivity::Creation refute_includes art, PublicActivity::Update refute_includes art, PublicActivity::Destruction end describe 'default options' do subject { article } specify { assert_includes subject, PublicActivity::Creation } specify { assert_includes subject, PublicActivity::Destruction } specify { assert_includes subject, PublicActivity::Update } specify do callbacks = subject._create_callbacks.select do |c| c.kind.eql?(:after) && c.filter == :activity_on_create end refute_empty callbacks end specify do callbacks = subject._update_callbacks.select do |c| c.kind.eql?(:after) && c.filter == :activity_on_update end refute_empty callbacks end specify do callbacks = subject._destroy_callbacks.select do |c| c.kind.eql?(:before) && c.filter == :activity_on_destroy end refute_empty callbacks end end it 'accepts :except option' do art = case PublicActivity.config.orm when :mongoid Class.new do include Mongoid::Document include Mongoid::Timestamps include PublicActivity::Model belongs_to :user field :name, type: String field :published, type: Boolean tracked except: [:create] end when :mongo_mapper Class.new do include MongoMapper::Document include PublicActivity::Model belongs_to :user key :name, String key :published, Boolean tracked except: [:create] timestamps! end when :active_record article(except: [:create]) end refute_includes art, PublicActivity::Creation assert_includes art, PublicActivity::Update assert_includes art, PublicActivity::Destruction end it 'accepts :only option' do art = case PublicActivity.config.orm when :mongoid Class.new do include Mongoid::Document include Mongoid::Timestamps include PublicActivity::Model belongs_to :user field :name, type: String field :published, type: Boolean tracked only: %i[create update] end when :mongo_mapper Class.new do include MongoMapper::Document include PublicActivity::Model belongs_to :user key :name, String key :published, Boolean tracked only: %i[create update] end when :active_record article(only: %I[create update]) end assert_includes art, PublicActivity::Creation refute_includes art, PublicActivity::Destruction assert_includes art, PublicActivity::Update end it 'accepts :owner option' do owner = mock('owner') subject.tracked(owner: owner) assert_equal subject.activity_owner_global, owner end it 'accepts :params option' do params = { a: 1 } subject.tracked(params: params) assert_equal subject.activity_params_global, params end it 'accepts :on option' do on = { a: -> {}, b: proc {} } subject.tracked(on: on) assert_equal subject.activity_hooks, on end it 'accepts :on option with string keys' do on = { 'a' => -> {} } subject.tracked(on: on) assert_equal subject.activity_hooks, on.symbolize_keys end it 'accepts :on values that are procs' do on = { unpassable: 1, proper: -> {}, proper_proc: proc {} } subject.tracked(on: on) assert_includes subject.activity_hooks, :proper assert_includes subject.activity_hooks, :proper_proc refute_includes subject.activity_hooks, :unpassable end describe 'global options' do subject { article(recipient: :test, owner: :test2, params: { a: 'b' }) } specify { assert_equal subject.activity_recipient_global, :test } specify { assert_equal subject.activity_owner_global, :test2 } specify { assert_equal subject.activity_params_global, a: 'b' } end end describe 'activity hooks' do subject do s = article s.activity_hooks = { test: hook } s end let(:hook) { -> {} } it 'retrieves hooks' do assert_same hook, subject.get_hook(:test) end it 'retrieves hooks by string keys' do assert_same hook, subject.get_hook('test') end it 'returns nil when no matching hook is present' do assert_same nil, subject.get_hook(:nonexistent) end it 'allows hooks to decide if activity should be created' do subject.tracked @article = subject.new(name: 'Some Name') PublicActivity.set_controller(mock('controller')) pf = proc { |model, controller| assert_same controller, PublicActivity.get_controller assert_equal model.name, 'Some Name' false } pt = proc { |model, controller| assert_same controller, PublicActivity.get_controller assert_equal model.name, 'Other Name' true # this will save the activity with *.update key } @article.class.activity_hooks = { create: pf, update: pt, destroy: pt } assert_empty @article.activities.to_a @article.save # create @article.name = 'Other Name' @article.save # update @article.destroy # destroy assert_equal @article.activities.count, 2 assert_equal @article.activities.first.key, 'article.update' end end def teardown PublicActivity.set_controller(nil) end end ================================================ FILE: test/test_view_helpers.rb ================================================ # frozen_string_literal: true require 'test_helper' describe 'ViewHelpers Rendering' do include PublicActivity::ViewHelpers # is this a proper test? it 'provides render_activity helper' do activity = mock('activity') activity.stubs(:is_a?).with(PublicActivity::Activity).returns(true) activity.expects(:render).with(self, {}) render_activity(activity) end it 'handles multiple activities' do activity = mock('activity') activity.expects(:render).with(self, {}) render_activities([activity]) end it 'flushes content_for between partials renderes' do @view_flow = mock('view_flow') @view_flow.expects(:set).twice.with('name', ActiveSupport::SafeBuffer.new) single_content_for('name', 'content') assert_equal @name, 'name' assert_equal @content, 'content' single_content_for('name', 'content2') assert_equal @name, 'name' assert_equal @content, 'content2' end def content_for(name, content) @name = name @content = content end end ================================================ FILE: test/views/custom/_layout.erb ================================================

Here be the custom layouts

<%= yield %> ================================================ FILE: test/views/custom/_test.erb ================================================ Custom Template Root <%= activity.id %> ================================================ FILE: test/views/layouts/_activity.erb ================================================

Here be the layouts

<%= yield %> ================================================ FILE: test/views/public_activity/_test.erb ================================================ <% if defined?(two) == 'local-variable' %> <% # output local_variable if defined %> <%= two %> <% else %> <%= p[:one] %>, <%= params['two'] %> <%= a.key %>, <%= activity.id %> <%= current_user %> <% end %>