Giter VIP home page Giter VIP logo

secretary-rails's Introduction

Secretary

Build Status

A note about the Gem name

There is another gem called secretary, which hasn't been updated since 2008 and is obsolete. This gem is called secretary-rails since secretary is already taken on RubyGems. However, the module name is the same, so using them together would be difficult.

What is it?

Light-weight model versioning for ActiveRecord 3.2+.

How does it work?

Whenever you save your model, a new version is saved. The changes are serialized and stored in the database, along with a version description, foreign keys to the object, and a foreign key to the user who saved the object.

Why is it better than [other versioning gem]?

  • It tracks associations.
  • It provides diffs (using the diffy gem).
  • It only stores the changes, not the whole object.
  • It is simple.

Compatibility

  • Rails 4+ (For 3.2 support, use secretary-rails ~> 1.0)
  • Ruby 2.0+
  • SQLite
  • MySQL
  • PostgreSQL

Dependencies

Installation

Add to your gemfile:

gem 'secretary-rails'

Run the install command, which will create a migration to add the versions table, and then run the migration:

bundle exec rails generate secretary:install
bundle exec rake db:migrate

Usage

Add the has_secretary macro to your model:

class Article < ActiveRecord::Base
  has_secretary
end

Congratulations, now your records are being versioned.

Tracking associations

This gem is built with the end-user in mind, so it doesn't track hidden associations (i.e. join models). However, you can tell it to track associated objects WITHIN their parent object's version by using the tracks_association macro. For example:

class Author < ActiveRecord::Base
  has_many :article_authors
  has_many :articles, through: :article_authors
end

class ArticleAuthor < ActiveRecord::Base
  belongs_to :article
  belongs_to :author
end

class Article < ActiveRecord::Base
  has_secretary

  has_many :article_authors
  has_many :authors, through: :article_authors
  tracks_association :authors
end

Now, when you save an Article, a new version won't be created for the new ArticleAuthor object(s). Instead, an array will be added to the Article's changes, which will include the information about the author(s).

You can also pass in multiple association names into tracks_association.

This also works on all other association types in the same way:

  • has_many
  • has_many :through
  • has_and_belongs_to_many
  • has_one
  • belongs_to

Dirty Associations

Secretary provides Rails-style dirty attributes for associations. Given an association has_many :pets, the methods available are:

  • pets_changed?
  • pets_were

Secretary also merges in the association changes into the standard Rails changes hash:

person.pets.to_a # => []

person.pets << Pet.new(name: "Spot")

person.pets_changed? # => true
person.changed?      # => true
person.pets_were     # => []
person.changes       # => { "pets" => [[], [{ "name" => "Spot" }]]}

Tracking Users

A version has an association to a user object, which tells you who created that version. The logged user is an attribute on the object being changed, so you can add it in via the controller:

class ArticlesController < ApplicationControler
  before_filter :get_object, only: [:show, :edit, :update, :destroy]
  before_filter :inject_logged_user, only: [:update]

  def create
    @article = Article.new(article_params)
    inject_logged_user
    # ...
  end

  # ...

  private

  def get_object
    @article = Article.find(params[:id])
  end

  def inject_logged_user
    @article.logged_user_id = @current_user.id
  end
end

Protip: Using outpost-secretary? This is taken care of for you. Just be sure to add the logged_user_id to your Strong Parameters.

Viewing Diffs

The Secretary::Version model allows you to see unix-style diffs of the changes, using the diffy gem. The diffs are represented as a hash, where the key is the name of the attribute, and the value is the Diffy::Diff object.

article = Article.new(headline: "Old Headline", body: "Lorem ipsum...")
article.save

article.update_attributes(headline: "Updated Headline", body: "Updated Body")

last_version = article.versions.last
puts last_version.attribute_diffs

{"headline"=>
  -Old Headline
\ No newline at end of file
+Updated Headline
\ No newline at end of file
,
 "body"=>
  -Lorem ipsum...
\ No newline at end of file
+Updated Body
\ No newline at end of file
}

This is just the simple text representation of the Diffy::Diff objects. Diffy also provides several other output formats. See diffy's README for more options.

Configuration

The install task will create an initializer for you with the following options:

  • user_class - The class for your user model.
  • ignored_attributes - The attributes which should always be ignored when generating a version, for every model, as an array of Strings.

Specifying which attributes to keep track of

Sometimes you have an attribute on your model that either isn't public (not in the form), or you just don't want to version. You can tell Secretary to ignore these attributes globally by setting Secretary.config.ignore_attributes. You can also ignore attributes on a per-model basis by using one of two options:

NOTE The attributes must be specified as Strings.

class Article < ActiveRecord::Base
  # Inclusion
  has_secretary on: ["headline", "body"]
end
class Article < ActiveRecord::Base
# Exclusion
  has_secretary except: ["published_at", "is_editable"]
end

By default, the versioned attributes are: the model's column names, minus the globally configured ignored_attributes, minus any excluded attributes you have set.

Using tracks_association adds those associations to the versioned_attributes array:

class Article < ActiveRecord::Base
  has_secretary on: ["headline"]

  has_many :images
  tracks_association :images
end

Article.versioned_attributes # => ["headline", "images"]

Changes vs. Versions

There is one aspect that may seem a bit confusing. The behavior of record.changes, and other Dirty attribute methods from ActiveModel, is preserved, so any attribute you change will be added to the record's changes. However, this does not necessarily mean that a version will be created, because you may have changed an attribute that isn't versioned. For example:

class Article < ActiveRecord::Base
  has_secretary on: ["headline", "body"]
end

article = Article.find(1)
article.changed? #=> false

article.slug = "new-slug-for-article" # "slug" is not versioned
article.changed? #=> true
article.changed #=> ["slug"]

article.versioned_changes #=> {}
article.save! # A new version isn't created!

This also goes for associations: if you change an association on a parent object, but in an "insignificant" way (i.e., no versioned attributes are changed), then that association won't be considered "changed" when it comes time to build the version.

Contributing

Fork it and send a pull request!

TODO

  • See Issues.
  • Add support for other ORM's besides ActiveRecord.
  • Associations are only tracked one-level deep, It would be nice to also track the changes of the association (i.e. recognize when an associated object was changed and show its changed, instead of just showing a whole new object).

Running Tests

Running the full suite requires that you have SQLite, MySQL, and Postgres servers all installed and running. Once you have them setup, setup the databases by running bundle exec rake test:setup. This will create the databases you need. You should only need to run this once.

If you get a message like FATAL: database "combustion_test" does not exist when running rake test:setup, it's okay (you can ignore it). The database gets created anyways.

Secretary uses the appraisals gem to run its tests across different versions of Rails.

Run rake -T to see all of the options for running the tests.

To run the full test suite:

$ bundle exec rake test

secretary-rails's People

Contributors

bricker avatar nakedsushi avatar quainjn avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

secretary-rails's Issues

Error: uninitialized constant User (NameError)

rails generate secretary:install
/home/sandro/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-4.2.5/lib/active_support/inflector/methods.rb:261:in const_get': uninitialized constant User (NameError) from /home/sandro/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-4.2.5/lib/active_support/inflector/methods.rb:261:inblock in constantize'

Inherited class associations don't work

class Audio
end

class Audio::DirectAudio < Audio
end

class Story
  has_many :audio
end

Story.last.audio << Audio::DirectAudio.new
undefined method [] for NilClass

Issue in Rails 4.2 with modify frozen ActiveSupport::HashWithIndifferentAccess

It appears there were changes in rails 4.2 that are causing this gem to not work.

The error is in dirty_associates line 51 according to the traceback.

can't modify frozen ActiveSupport::HashWithIndifferentAccess
        from /home/vagrant/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/activesupport-4.2.0/lib/active_support/hash_with_indifferent_access.rb:97:in `[]='
        from /home/vagrant/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/activesupport-4.2.0/lib/active_support/hash_with_indifferent_access.rb:97:in `[]='
        from /home/vagrant/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/secretary-rails-1.1.1/lib/secretary/dirty_associations.rb:51:in `roles_will_change!'
        from /home/vagrant/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/secretary-rails-1.1.1/lib/secretary/dirty_associations/collection.rb:51:in `prepare_to_change_association'

Psych::SyntaxError: (<unknown>): found unexpected end of stream while scanning a quoted scalar at line 488 column 5

This is happening on some Secretary::Version records, though the cause is unknown.

Secretary::Version.find(3309)
#=>Psych::SyntaxError: (<unknown>): found unexpected end of stream while scanning a quoted scalar at line 488 column 5

It also happens when you use a where clause, though you will get back a relation of objects that are broken and you can't view all of the attributes of the model(in which case that same error gets raised).

Full ActiveModel::Dirty API not implemented

  • changed does not include associations
  • changed_attributes does not include associations
  • attribute_change does not exist for association
  • reset_attribute! doesn't exist for association
  • attribute_will_change! doesn't exist for association

Assigning a has_one association to the same object dirties the parent object

For example:

image = Image.create(url: "http://kitties.com/meow.jpg")
article = Article.find(1)

article.image = image
article.changed? #=> true
article.save!

article.changed? #=> false
article.image = image
article.change? #=> true

Assigning a has_one association should be smart enough to know when the object being assigned is the same.

::tracks_association fails on Rails 4.1.1

This seems to have something to do with defining callbacks on the association. It throws a NotImplementedError.

The stack-trace looks like this:

.bundle/ruby/2.0.0/gems/activerecord-4.1.1/lib/active_record/associations/builder/association.rb:124:in `valid_dependent_options'
.bundle/ruby/2.0.0/gems/activerecord-4.1.1/lib/active_record/associations/builder/association.rb:130:in `add_before_destroy_callbacks'
.bundle/ruby/2.0.0/gems/activerecord-4.1.1/lib/active_record/associations/builder/association.rb:88:in `define_callbacks'
.bundle/ruby/2.0.0/gems/activerecord-4.1.1/lib/active_record/associations/builder/collection_association.rb:27:in `define_callbacks'
.bundle/ruby/2.0.0/gems/secretary-rails-1.1.0/lib/secretary/dirty_associations/collection.rb:18:in `add_collection_callbacks'
.bundle/ruby/2.0.0/gems/secretary-rails-1.1.0/lib/secretary/tracks_association.rb:91:in `block in tracks_association'
.bundle/ruby/2.0.0/gems/secretary-rails-1.1.0/lib/secretary/tracks_association.rb:58:in `each'
.bundle/ruby/2.0.0/gems/secretary-rails-1.1.0/lib/secretary/tracks_association.rb:58:in `tracks_association'
app/models/order.rb:16:in `<class:Order>'
app/models/order.rb:1:in `<top (required)>'

I had a quick look at the implementation of valid_dependent_options and it now just throws NotImplementedError. Obviously the implementations of this method have moved into the association sub-classes. Unfortunately I don't know enough about how the association callbacks work in order to debug this any further.

Rails 4.2.1: secretary-rails breaks auto-save for `has_many :through`

We upgraded SCPRv4 to Rails 4.2.1 this week and found that trying to trying to autosave rundowns to associate episodes and segments stopped working. Basically it tries to save the rundown without an episode_id. In a test app, I was able to create minimal models that showed that a) the default Rails behavior for saving works and b) that the error condition is created when tracks_association is added for the rundowns.

Minimal models:

class Episode < ActiveRecord::Base
  has_secretary
  has_many :rundowns
  has_many :segments, through: :rundowns

  tracks_association :rundowns
end

class Rundown < ActiveRecord::Base
  belongs_to :episode
  belongs_to :segment
end

class Segment < ActiveRecord::Base
  has_many :rundowns
  has_many :episodes, through: :rundowns
end

Without tracks_association:

irb(main):001:0> e = Episode.new
=> #<Episode id: nil, created_at: nil, updated_at: nil>
irb(main):002:0> s = Segment.new
=> #<Segment id: nil, created_at: nil, updated_at: nil>
irb(main):003:0> s.save
   (0.2ms)  begin transaction
  SQL (1.5ms)  INSERT INTO "segments" ("created_at", "updated_at") VALUES (?, ?)  [["created_at", "2015-05-08 14:00:29.041412"], ["updated_at", "2015-05-08 14:00:29.041412"]]
   (0.7ms)  commit transaction
=> true
irb(main):004:0> e.segments << s
=> #<ActiveRecord::Associations::CollectionProxy [#<Segment id: 5, created_at: "2015-05-08 14:00:29", updated_at: "2015-05-08 14:00:29">]>
irb(main):005:0> e.save
   (0.1ms)  begin transaction
  SQL (2.2ms)  INSERT INTO "episodes" ("created_at", "updated_at") VALUES (?, ?)  [["created_at", "2015-05-08 14:00:35.643652"], ["updated_at", "2015-05-08 14:00:35.643652"]]
  SQL (0.5ms)  INSERT INTO "rundowns" ("segment_id", "episode_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["segment_id", 5], ["episode_id", 3], ["created_at", "2015-05-08 14:00:35.647912"], ["updated_at", "2015-05-08 14:00:35.647912"]]
   (0.8ms)  commit transaction
=> true

With tracks_association:

irb(main):007:0> e = Episode.new
=> #<Episode id: nil, created_at: nil, updated_at: nil>
irb(main):008:0> s = Segment.new
=> #<Segment id: nil, created_at: nil, updated_at: nil>
irb(main):009:0> s.save
   (0.1ms)  begin transaction
  SQL (0.5ms)  INSERT INTO "segments" ("created_at", "updated_at") VALUES (?, ?)  [["created_at", "2015-05-08 14:00:58.889439"], ["updated_at", "2015-05-08 14:00:58.889439"]]
   (2.2ms)  commit transaction
=> true
irb(main):010:0> e.segments << s
=> #<ActiveRecord::Associations::CollectionProxy [#<Segment id: 6, created_at: "2015-05-08 14:00:58", updated_at: "2015-05-08 14:00:58">]>
irb(main):011:0> e.save
   (0.1ms)  begin transaction
  SQL (0.5ms)  INSERT INTO "episodes" ("created_at", "updated_at") VALUES (?, ?)  [["created_at", "2015-05-08 14:01:06.545339"], ["updated_at", "2015-05-08 14:01:06.545339"]]
  SQL (1.5ms)  INSERT INTO "rundowns" ("segment_id", "created_at", "updated_at") VALUES (?, ?, ?)  [["segment_id", 6], ["created_at", "2015-05-08 14:01:06.547891"], ["updated_at", "2015-05-08 14:01:06.547891"]]
SQLite3::ConstraintException: NOT NULL constraint failed: rundowns.episode_id: INSERT INTO "rundowns" ("segment_id", "created_at", "updated_at") VALUES (?, ?, ?)
   (1.8ms)  rollback transaction
ActiveRecord::StatementInvalid: SQLite3::ConstraintException: NOT NULL constraint failed: rundowns.episode_id: INSERT INTO "rundowns" ("segment_id", "created_at", "updated_at") VALUES (?, ?, ?)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.