Code Monkey home page Code Monkey logo

draftsman's Introduction

Project status

๐Ÿšจ Drfatsman is looking for a new Steward ๐Ÿšจ

Draftsman v0.8.0.dev

Build Status

Draftsman is a Ruby gem that lets you create draft versions of your database records. If you're developing a system in need of simple drafts or a publishing approval queue, then Draftsman just might be what you need.

  • The largest risk at this time is functionality that assists with publishing or reverting dependencies through associations (for example, "publishing" a child also publishes its parent if it's a new item). We'll be putting this functionality through its paces in the coming months.
  • The RSpec tests are lacking in some areas, so I will be adding to those over time as well. (Unfortunately, this gem was not developed with TDD best practices because it was lifted from PaperTrail and modified from there.)

This gem is inspired by the Kentouzu gem, which is based heavily on PaperTrail. In fact, much of the structure for this gem emulates PaperTrail (because it works beautifully). You should definitely check out PaperTrail and its source: it's a nice clean example of a gem that hooks into Rails and Sinatra.

Features

  • Provides API for storing drafts of creations, updates, and destroys.
  • A max of one draft per record (via belongs_to association).
  • Does not store drafts for updates that don't change anything.
  • Allows you to specify attributes (by inclusion or exclusion) that must change for a draft to be stored.
  • Ability to query drafts based on the current drafted item, or query all drafts polymorphically on the drafts table.
  • publish! and revert! methods for drafts also handle any dependent drafts so you don't end up with orphaned records.
  • Allows you to get at every draft, even if the schema has since changed.
  • Automatically records who was responsible via your controller. Draftsman calls current_user by default if it exists, but you can have it call any method you like.
  • Allows you to store arbitrary model-level metadata with each draft (useful for filtering).
  • Allows you to store arbitrary controller-level information with each draft (e.g., remote IP, current account ID).
  • Only saves drafts when you explicitly tell it to via instance methods like save_draft and draft_destruction.
  • Stores everything in a single database table by default (generates migration for you), or you can use separate tables for separate models.
  • Supports custom draft classes so different models' drafts can have different behavior.
  • Supports custom name for draft association.
  • Supports before, after, and around callbacks on each draft persistence method, such as before_save_draft or around_draft_destruction.
  • Threadsafe.

Compatibility

Compatible with ActiveRecord 4 and 5.

Works well with Rails, Sinatra, or any other application that depends on ActiveRecord.

Installation

Rails 4 and 5

Add Draftsman to your Gemfile.

gem 'draftsman', '~> 0.7.1'

Or if you want to grab the latest from master:

gem 'draftsman', github: 'jmfederico/draftsman'

Generate a migration which will add a drafts table to your database.

$ rails g draftsman:install

You can pass zero or any combination of these options to the generator:

$ rails g draftsman:install --skip-initializer  # Skip generation of the boilerplate initializer at
                                                # `config/initializers/draftsman.rb`.

$ rails g draftsman:install --with-changes      # Store changeset (diff) with each draft.

$ rails g draftsman:install --with-pg-json      # Use PostgreSQL JSON data type for serialized data.

Run the migration(s).

$ rake db:migrate

Add draft_id, published_at, and trashed_at attributes to the models you want to have drafts on. trashed_at is optional if you don't want to store drafts for destroys.

$ rails g migration add_drafts_to_widgets draft_id:integer published_at:timestamp trashed_at:timestamp
$ rake db:migrate

Add has_drafts to the models you want to have drafts on.

Lastly, if your controllers have a current_user method, you can easily track who is responsible for changes by adding a controller filter.

class ApplicationController
  before_action :set_draftsman_whodunnit
end

Sinatra

In order to configure Draftsman for usage with Sinatra, your Sinatra app must be using ActiveRecord 4 or greater. It is also recommended to use the Sinatra ActiveRecord Extension or something similar for managing your application's ActiveRecord connection in a manner similar to the way Rails does. If using the aforementioned Sinatra ActiveRecord Extension, steps for setting up your app with Draftsman will look something like this:

Add Draftsman to your Gemfile.

gem 'draftsman', github: 'jmfederico/draftsman'

Generate a migration to add a drafts table to your database.

$ rake db:create_migration NAME=create_drafts

Copy contents of create_drafts.rb into the create_drafts migration that was generated into your db/migrate directory.

Run the migration(s).

$ rake db:migrate

Add draft_id, published_at, and trashed_at attributes to the models you want to have drafts on. (trashed_at is optional if you don't want to store drafts for destroys.)

Add has_drafts to the models you want to have drafts on.

Draftsman provides a helper extension that acts similarly to the controller mixin it provides for Rails applications.

It will set Draftsman::Draft#whodunnit to whatever is returned by a method named user_for_draftsman, which you can define inside your Sinatra application. (By default, it attempts to invoke a method named current_user.)

If you're using the modular Sinatra::Base style of application, you will need to register the extension:

# my_app.rb
require 'sinatra/base'

class MyApp < Sinatra::Base
  register Draftsman::Sinatra
end

API Summary

has_draft Options

To get started, add a call to has_drafts to your model. has_drafts accepts the following options:

:class_name

The name of a custom Draft class. This class should inherit from Draftsman::Draft. A global default can be set for this using Draftsman.draft_class_name= if the default of Draftsman::Draft needs to be overridden.

:ignore

An array of attributes for which an update to a Draft will not be stored if they are the only ones changed.

:only

Inverse of ignore - a new Draft will be created only for these attributes if supplied. It's recommended that you only specify optional attributes for this (that can be empty).

:skip

Fields to ignore completely. As with ignore, updates to these fields will not create a new Draft. In addition, these fields will not be included in the serialized versions of the object whenever a new Draft is created.

:meta

A hash of extra data to store. You must add a column to the drafts table for each key. Values are objects or procs (which are called with self, i.e. the model with the has_drafts). See Draftsman::Controller.info_for_draftsman for an example of how to store data from the controller.

:draft

The name to use for the draft association shortcut method. Default is :draft.

:published_at

The name to use for the method which returns the published timestamp. Default is published_at.

:trashed_at

The name to use for the method which returns the soft delete timestamp. Default is trashed_at.

:publish_options

The hash of options that will be passed to #save when publishing the draft. Default is { validate: false }

Drafted Item Class Methods

When you install the Draftsman gem, you get these methods on each model class:

# Returns whether or not `has_draft` has been called on the model.
Widget.draftable?

# Returns whether or not a `trashed_at` timestamp is set up on this model.
Widget.trashable?

Drafted Item Instance Methods

When you call has_drafts in your model, you get the following methods. See the "Basic Usage" section below for more context on where these methods fit into your data's lifecycle.

# Returns this widget's draft. You can customize the name of this association.
widget.draft

# Returns whether or not this widget has a draft.
widget.draft?

# Saves record and records a draft for the object's creation or update. Much
# like `ActiveRecord`'s `#save`, returns `true` or `false` depending on whether
# or not the objects passed validation and the save was successful.
widget.save_draft

# Trashes object and records a draft for a `destroy` event. (The `trashed_at`
# attribute must be set up on your model for this to work.)
widget.draft_destruction

# Returns whether or not this item has been published at any point in its
# lifecycle.
widget.published?

# Returns whether or not this item has been trashed via `#draft_destruction`.
widget.trashed?

Drafted Item Scopes

You also get these scopes added to your model for your querying enjoyment:

Widget.drafted    # Limits to items that have drafts. Best used in an "admin" area in your application.
Widget.published  # Limits to items that have been published at some point in their lifecycles. Best used in a "public" area in your application.
Widget.trashed    # Limits to items that have been drafted for deletion (but not fully committed for deletion). Best used in an "admin" area in your application.
Widget.live       # Limits to items that have not been drafted for deletion. Best used in an "admin" area in your application.

These scopes optionally take a referenced_table_name argument for constructing more advanced queries using .includes eager loading or .joins. This reduces ambiguity both for SQL queries and for your Ruby code.

# Query live `widgets` and `gears` without ambiguity.
Widget.live.includes(:gears, :sprockets).live(:gears)

Draft Class Methods

The Draftsman::Draft class has the following scopes:

# Returns all drafts created by the `create` event.
Draftsman::Draft.creates

# Returns all drafts created by the `update` event.
Draftsman::Draft.updates

# Returns all drafts created by the `destroy` event.
Draftsman::Draft.destroys

Draft Instance Methods

And a Draftsman::Draft instance has these methods:

# Return the associated item in its state before the draft.
draft.item

# Return the object in its state held by the draft.
draft.reify

# Returns what changed in this draft. Similar to `ActiveModel::Dirty#changes`.
# Returns `nil` if your `drafts` table does not have an `object_changes` text
# column.
draft.changeset

# Returns whether or not this is a `create` event.
draft.create?

# Returns whether or not this is an `update` event.
draft.update?

# Returns whether or not this is a `destroy` event.
draft.destroy?

# Publishes this draft's associated `item`, publishes its `item`'s dependencies,
# and destroys itself.
# -  For `create` drafts, adds a value for the `published_at` timestamp on the
#    item and destroys the draft.
# -  For `update` drafts, applies the drafted changes to the item and destroys
#    the draft.
# -  For `destroy` drafts, destroys the item and the draft.
#
# Params:
# -  A hash of options that get merged with `publish_options` defined in
#    `has_drafts` and passed to `item.save`.
draft.publish!

# Reverts this draft's associated `item` to its previous state, reverts its
# `item`'s dependencies, and destroys itself.
# -  For `create` drafts, destroys the draft and the item.
# -  For `update` drafts, destroys the draft only.
# -  For `destroy` drafts, destroys the draft and undoes the `trashed_at`
#    timestamp on the item. If a draft was drafted for destroy, restores the
#    draft.
draft.revert!

# Returns related draft dependencies that would be along for the ride for a
# `publish!` action.
draft.draft_publication_dependencies

# Returns related draft dependencies that would be along for the ride for a
# `revert!` action.
draft.draft_reversion_dependencies

Callbacks

Draftsman supports callbacks for draft saves and destroys. These callbacks can be defined in any model that has_drafts.

Draft callbacks work similarly to ActiveRecord callbacks; pass any functions that you would like called before/around/after a draft persistence method.

Available callbacks:

before_save_draft         # called before draft is saved
around_save_draft         # called function must yield to `save_draft`
after_draft_save          # called after draft is saved

before_draft_destruction  # called before item is destroyed as a draft
around_draft_destruction  # called function must yield to `draft_destruction`
after_draft_destruction   # called after item is destroyed as a draft

Note that callbacks must be defined after your call to has_drafts.

Basic Usage

A basic widgets admin controller in Rails that saves all of the user's actions as drafts would look something like this. It also presents all data in its drafted form, if a draft exists.

class Admin::WidgetsController < Admin::BaseController
  before_action :find_widget,  only: [:show, :edit, :update, :destroy]
  before_action :reify_widget, only: [:show, :edit]

  def index
    # The `live` scope gives us widgets that aren't in the trash.
    # It's also strongly recommended that you eagerly-load the `draft`
    # association via `includes` so you don't keep hitting your database for
    # each draft.
    @widgets = Widget.live.includes(:draft).order(:title)

    # Load drafted versions of each widget.
    @widgets.map! { |widget| widget.draft.reify if widget.draft? }
  end

  def show
  end

  def new
    @widget = Widget.new
  end

  def create
    @widget = Widget.new(widget_params)

    # Instead of calling `save`, you call `save_draft` to save it as a draft.
    if @widget.save_draft
      flash[:success] = 'Draft of widget saved successfully.'
      redirect_to [:admin, @widget]
    else
      flash[:error] = 'There was an error saving the draft.'
      render :new
    end
  end

  def edit
  end

  def update
    @widget.attributes = widget_params

    # Instead of calling `update`, you call `save_draft` to save it as a draft.
    if @widget.save_draft
      flash[:success] = 'Draft of widget saved successfully.'
      redirect_to [:admin, @widget]
    else
      flash[:error] = 'There was an error saving the draft.'
      render :edit
    end
  end

  def destroy
    # Instead of calling `destroy`, you call `draft_destruction` to "trash" it as a draft
    @widget.draft_destruction
    flash[:success] = 'The widget was moved to the trash.'
    redirect_to admin_widgets_path
  end

private

  # Finds non-trashed widget by `params[:id]`.
  def find_widget
    @widget = Widget.live.find(params[:id])
  end

  # If the widget has a draft, load that version of it.
  def reify_widget
    @widget = @widget.draft.reify if @widget.draft?
  end

  # Strong parameters for widget form.
  def widget_params
    params.require(:widget).permit(:title)
  end
end

And "public" controllers (let's say read-only for this simple example) would ignore drafts entirely via the published scope. This also allows items to be "trashed" for admins but still accessible to the public until that deletion is committed.

class WidgetsController < ApplicationController
  def index
    # The `published` scope gives us widgets that have been committed to be
    # viewed by non-admin users.
    @widgets = Widget.published.order(:title)
  end

  def show
    @widget = Widget.published.find(params[:id])
  end
end

Obviously, you can use the scopes that Draftsman provides however you would like in any case.

Lastly, a drafts controller could be provided for admin users to see all drafts, no matter the type of record (thanks to ActiveRecord's polymorphic associations). From there, they could choose to revert or publish any draft listed, or any other workflow action that you would like for your application to provide for drafts.

class Admin::DraftsController < Admin::BaseController
  before_action :find_draft, only: [:show, :update, :destroy]

  def index
    @drafts = Draftsman::Draft.includes(:item).order(updated_at: :desc)
  end

  def show
  end

  # Post draft ID here to publish it
  def update
    # Call `draft_publication_dependencies` to check if any other drafted
    # records should be published along with this `@draft`.
    @dependencies = @draft.draft_publication_dependencies

    # If you would like to warn the user about dependent drafts that would need
    # to be published along with this one, you would implement an
    # `app/views/drafts/update.html.erb` view template. In that view template,
    # you could list the `@dependencies` and show a button posting back to this
    # action with a name of `commit_publication`. (The button's being clicked
    # indicates to your application that the user accepts that the dependencies
    # should be published along with the `@draft`, thus avoiding orphaned
    # records).
    if @dependencies.empty? || params[:commit_publication]
      @draft.publish!
      flash[:success] = 'The draft was published successfully.'
      redirect_to [:admin, :drafts]
    else
      # Renders `app/views/drafts/update.html.erb`
    end
  end

  # Post draft ID here to revert it
  def destroy
    # Call `draft_reversion_dependencies` to check if any other drafted records
    # should be reverted along with this `@draft`.
    @dependencies = @draft.draft_reversion_dependencies

    # If you would like to warn the user about dependent drafts that would need
    # to be reverted along with this one, you would implement an
    # `app/views/drafts/destroy.html.erb` view template. In that view template,
    # you could list the `@dependencies` and show a button posting back to this
    # action with a name of `commit_reversion`. (The button's being clicked
    # indicates to your application that the user accepts that the dependencies
    # should be reverted along with the `@draft`, thus avoiding orphaned
    # records).
    if @dependencies.empty? || params[:commit_reversion]
      @draft.revert!
      flash[:success] = 'The draft was reverted successfully.'
      redirect_to [:admin, :drafts]
    else
      # Renders `app/views/drafts/destroy.html.erb`
    end
  end

private

  # Finds draft by `params[:id]`.
  def find_draft
    @draft = Draftsman::Draft.find(params[:id])
  end
end

If you would like your Widget to have callbacks, it might look something like this:

class Widget < ActiveRecord::Base
  has_drafts

  before_save_draft :say_hi
  around_save_draft :surround_update

private

  def say_hi
    self.some_attr = 'Hi!'
  end

  def surround_update
    if self.persisted?
      # do something before update
      yield
      # do something after update
    else
      yield
    end
  end
end

Differences from PaperTrail

If you are familiar with the PaperTrail gem, some parts of the Draftsman gem will look very familiar.

However, there are some differences:

  • PaperTrail hooks into ActiveRecord callbacks so that versions can be saved automatically with your normal CRUD operations (#save, #create, #update, #destroy, etc.). Draftsman requires that you explicitly call its own CRUD methods in order to save a draft (#save_draft and draft_destruction).

  • PaperTrail's Version#object column looks "backward" and records the object's state before the changes occurred. Because drafts record changes as they will look in the future, they must work differently. Draftsman's Draft#object records the object's state after changes are applied to the master object. But destroy drafts record the object as it was before it was destroyed (in case you want the option of reverting the destroy later and restoring the drafted item back to its original state).

Semantic Versioning

Like many Ruby gems, Draftsman honors the concepts behind semantic versioning:

Given a version number MAJOR.MINOR.PATCH, increment the:

  1. MAJOR version when you make incompatible API changes,
  2. MINOR version when you add functionality in a backwards-compatible manner, and
  3. PATCH version when you make backwards-compatible bug fixes.

Contributing

If you feel like you can add something useful to Draftsman, then don't hesitate to contribute! To make sure your fix/feature has a high chance of being included, please do the following:

  1. Fork the repo.

  2. Run bundle install.

  3. Run RAILS_ENV=test bundle exec rake -f spec/dummy/Rakefile db:schema:load to load test database schema.

  4. Add at least one test for your change. Only refactoring and documentation changes require no new tests. If you are adding functionality or fixing a bug, you need a test!

  5. Make all tests pass by running rake.

  6. Push to your fork and submit a pull request.

I can't guarantee that I will accept the change, but if I don't, I will be sure to let you know why.

Here are some things that will increase the chance that your pull request is accepted, taken straight from the Ruby on Rails guide:

  • Use Rails idioms
  • Include tests that fail without your code, and pass with it
  • Update the documentation, guides, or whatever is affected by your contribution

This gem is a work in progress. I am adding specs as I need features in my application. Please add missing ones as you work on features or find bugs!

License

Copyright 2013-2016 Minimal Orange, LLC.

Draftsman is released under the MIT License.

draftsman's People

Contributors

bdunham avatar chrisdpeters avatar defbyte avatar iggant avatar jmfederico avatar jokius avatar mrcherry avatar npafundi avatar npezza93 avatar nuclearghost avatar tongboy avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

draftsman's Issues

Update specs to use new community standards

  1. Get rid of its.
  2. Use expect instead of shoulda matchers:
it 'adds a user' do
  expect(user.count).to eql 3
end
  1. Hardcode version numbers into development dependencies. Right now when you run Bundler, it installs version of RSpec and other gems that are incompatible with the code I wrote.

Uh oh, ActiveSupport::DeprecationException error when running generated migrations

๐ŸŒด Aloha! ๐ŸŒด

thanks very much for this gem! I'd been working on a very lo-fi version of this functionality, then said to heck with it and decided to check out your gem. I work on a lot of projects where publishing workflows are A Thing โ„ข๏ธ

So, I started to fire things up, got this error, and wanted to pass on a quick fix with PR #38

I hope to contribute more in the future, thanks again.

draft_destroying dependencies doesn't take into account draft association changes

Hey Chris, this is a weird one, and might be worth a conversation.

Let's say you have Book and Shelf models. A Shelf has_many books, and a Book can only be on one Shelf at any given time.

Further, assume some Book is on Shelf 1, and you decide you want to move it to Shelf 2, and this is done via draftsman. Your Book will have an association of Shelf 1 (its old location), and the Book's draft will have an association of Shelf 2 (its new location).

If you draft_destroy Shelf 1, the Book should still exist because its draft is on Shelf 2. However, currently draft_destroy handles destroying all dependencies. The issue is that it uses all non-drafted dependencies, regardless of whether the association has changed.

In the case above, this would mean that the Book would be draft_destroyed when Shelf 1 was draft destroyed, even though the Book('s draft) is on Shelf 2.

I tried to simplify the example above, but this was somewhat confusing to write up. If I can clarify anything, please let me know. If you'd like to communicate another way (email/skype/phone), we would be more than happy to talk about it, since there doesn't seem to be an obvious solution.

Thanks Chris!

irb: warn: can't alias context from irb_context.

when i load rails console with the latest rspec-rails (3.5.2) together with draftsman i get this warning.

Loading development environment (Rails 4.2.7.1)
irb: warn: can't alias context from irb_context.
2.2.5 :001 >

Performance: add options on `has_drafts` to whitelist or blacklist dependent associations

Sometimes we may want to setup an aliased association with a scope, while including the original association as well.

I recently ran across the need to do something like this:

has_drafts

has_many :widgets
has_many :live_widgets, -> { live(:widgets) }, class_name: 'Widget'

The problem is that Draftsman will inspect both of those associations when querying for reversion and publication dependencies, which is unnecessary in many cases.

It makes sense to allow the option to only have Draftsman query one (or even neither) of those associations when identifying publication and reversion dependencies.

The API could possibly look like this:

has_drafts only_dependencies: [:widgets]

Or:

has_drafts without_dependencies: [:live_widgets]

Exception on draft_destroy when has_one association is nil

If a model's has_one association is nil, draft_destroy will throw an exception.

For example, if I have a Parent which has_one :child, but the child doesn't exist yet (so the association is nil), a draft_destroy will throw a NoMethodError.

NoMethodError - undefined method `draft?' for nil:NilClass:
  <path>/draftsman/lib/draftsman/model.rb:282:in `block (4 levels) in draft_destroy'
  <path>/draftsman/lib/draftsman/model.rb:279:in `block (3 levels) in draft_destroy'
  <path>/draftsman/lib/draftsman/model.rb:270:in `block (2 levels) in draft_destroy'

This will occur in traditional and polymorphic has_one associations.

Edit: By simply adding a has_one association to the Parent class in the current dummy specs, you'll find that 29 specs fail.

Add option to not stash drafted data separately

There are scenarios where it makes sense to stash drafted data and require reification to get it back. And #51 could potentially make it more performant in most situations.

There are other scenarios where you may be creating a service that is only concerned with drafted data, so reification is not required or necessary. I'm finding myself in that situation with my current project, and I'd imagine that others would too.

This could be done via a setting or by simply removing the object column from the database. I am open to comments about which would be more appropriate. I'm leaning toward making it an explicit setting so that the intention is documented somewhere in the application's source code.

  • Add Draftsman.stash_drafted_changes config option
    • Add to generator
  • Apply new logic to Model#save_draft method
  • Apply new logic to Draft#reify method
  • Apply new logic to Draft#publish! method
  • Apply new logic to Draft#revert! method
  • Apply new logic to Model#draft_destruction - delete record entirely and stash it in Draft
  • Update documentation

Rails 5 migration error

The first line of the migration file it generate when you run rails g draftsman:install looks like this:

class CreateDrafts < ActiveRecord::Migration

If you run rake db:migrate rails throws an error:

rake aborted!
StandardError: An error has occurred, this and all later migrations canceled:

Directly inheriting from ActiveRecord::Migration is not supported. Please specify the Rails release the migration was written for:

class CreateDrafts < ActiveRecord::Migration[4.2]

I think the error message says it all! The code outputted by the generator should not directly inherit from ActiveRecord::Migration.

`reify` doesn't work when model has an enum attribute

I have a model defined like this:

class Question < ActiveRecord::Base
    ...
    enum question_response_type: { radio: 0, text: 1 }
    ...
end

When I try to call the reify method on a question draft, ActiveRecord throws an error:

ArgumentError: You tried to define an enum named "question_response_type" on the model "Question", but this will generate a instance method "question_response_type=", which is already defined by another enum.

Is there any way around this?

`skip` option not persisting skipped values correctly

This demonstrates the bug:

class Skipper < ActiveRecord::Base
  has_drafts skip: [:skip_me]
end

skipper = Skipper.create!(:name => 'Bob', :skip_me => 'Skipped 1')

skipper.name = 'Sam'
skipper.skip_me = 'Skipped 2'
skipper.draft_update
skipper.reload

skipper.skip_me
=> 'Skipped 1' # Should be 'Skipped 2'

JSON::ParserError when draft_destroying a widget which was just created

Hey Chris,

It's been a (very) long time, but I'm finally looping back around to putting drafts into our application. Today I ran across an issue, and I have a 100% reproducible case.

I created a simple app with the Postgres JSON data type (and used the install switch to enable it for drafts). The app has a single table and model, Widget, and I added drafts to this table.

If I create a draft Widget, then later draft_destroy this Widget (without publishing in between), I get a JSON::ParserError.

For example, in the console, I can do the following to reproduce:

w = Widget.new(some_attr: "hello")
w.draft_creation

At this point, I can see the Widget and draft, and all looks good. Running w.draft_destroy will result in:

JSON::ParserError: 795: unexpected token at '---
id: 1
item_type: Widget
item_id: 1
event: create
whodunnit:
object:
  id: 1
  some_attr: hi
  created_at: '2015-03-23T22:42:15.628Z'
  updated_at: '2015-03-23T22:42:15.628Z'
  draft_id:
  published_at:
  trashed_at:
previous_draft:
created_at: 2015-03-23 22:42:15.651663000 Z
updated_at: 2015-03-23 22:42:15.651663000 Z
'

I'm not sure if we're supposed to handle draft deletion differently for non-published content. If so, I can just change what I'm doing.

Let me know if you need more info!

Thanks!
Nick

Add Draftsman.ignore_draft_id_attribute setting

The purpose of this setting will be to not include draft_id in Draft#object and Draft#object_changes. It will be set to ignore by default.

When changed from the default, extra unneeded data is stored along with the draft, along with the potential to have unexpected results during object reification.

I'll need to experiment with this in the previous_draft attribute to make sure that omission of this data doesn't cause problems when reifying a trashed draft.

Sinatra extension should not use Sinatra base namespace

Hi, I'm the maintainer of PaperTrail. We just had an issue filed against the PaperTrail repo, paper-trail-gem/paper_trail#477, which was being caused by this gem.

I see that your gem here was cloned off of the PaperTrail source at one point. I wanted to advise you to modify your Sinatra extension so that it is not namespaced under the Sinatra module, as this can cause conflicts when other libraries look to see if Sinatra is defined. For more details see paper-trail-gem/paper_trail#277.

I would recommend simply adjusting your Sinatra module so that it is namespaced under the Draftsman module. You can mimick what we are doing on PaperTrail if that helps you.

Included model scopes break on includes and joins

Example query:

@content_template = current_site.content_templates.live
                                                  .includes(:draft, blocks: :draft, displays: :draft)
                                                  .where(blocks: { trashed_at: nil }, displays: { trashed_at: nil })
                                                  .find(params[:id])

Throws such an exception (in PostgreSQL):

PG::AmbiguousColumn: ERROR: column reference "trashed_at" is ambiguous LINE 1: ...id" WHERE "content_templates"."site_id" = $1 AND (trashed_at... ^ : SELECT DISTINCT "content_templates"."id" FROM "content_templates" LEFT OUTER JOIN "drafts" ON "drafts"."id" = "content_templates"."draft_id" LEFT OUTER JOIN "blocks" ON "blocks"."content_template_id" = "content_templates"."id" LEFT OUTER JOIN "drafts" "drafts_blocks" ON "drafts_blocks"."id" = "blocks"."draft_id" LEFT OUTER JOIN "displays" ON "displays"."content_template_id" = "content_templates"."id" LEFT OUTER JOIN "drafts" "drafts_displays" ON "drafts_displays"."id" = "displays"."draft_id" WHERE "content_templates"."site_id" = $1 AND (trashed_at IS NULL) AND "blocks"."trashed_at" IS NULL AND "displays"."trashed_at" IS NULL AND "content_templates"."id" = $2 LIMIT 1

NoMethodError in DraftsController#update undefined method `to_i` for Hash

My User model is a Devise model and I'm using the trackable functionality. If I attempt to publish a change to the User model with object_changes unrelated to devise, I receive this error:

screenshot from 2016-04-09 14-10-59

It seems related to this and this.

I know where the problem is happening, I just don't know enough about the issue I'm running into nor do I feel proficient in Ruby enough yet to know how I can help get it fixed or if it can be. The problem is happening during the reify call:

def reify
    without_identity_map do
      if !self.previous_draft.nil?
        reify_previous_draft.reify
      elsif !self.object.nil?

        [...]

        # The problem is happening in this loop when the key is equal to current_sign_in_ip or last_sign_in_ip
        attrs.each do |key, value|
          # Skip counter_cache columns
          if model.respond_to?("#{key}=") && !key.end_with?('_count')
            model.send "#{key}=", value
          elsif !key.end_with?('_count')
            logger.warn "Attribute #{key} does not exist on #{item_type} (Draft ID: #{id})."
          end
        end

        model.send "#{model.class.draft_association_name}=", self
        model
      end
    end
  end

I would love to help fix this if I better understood why setting these attributes was throwing this exception.

Updating Drafts with Attachments

Came across this issue when working with drafts that may have attachments on rails 3.2.8 (although suspect it affects newer versions).

Attachments are saved on save but doesn't look like draft_update triggers this method so images get marked to be saved but never moved since the save method doesn't get triggered.

While this is in an issue with paperclip, curious if there's a work around that you maybe able to recommend?

Tests for a model including an enum

Per discussion in #34, we need to have some tests to make sure that Draftsman works with models containing enums moving forward.

If anyone would like to contribute, let us know. Otherwise, I'll get to this soon.

Write tests for Draftsman.draft_class_name setting

This configuration doesn't seem to be picked up by the model:

Draftsman.draft_class_name = 'Draft'

It was working earlier running from my Mac on Rails 4.0. (I'm now running the app in Ubuntu, Rails 4.2)

In the meantime, I have to configure has_drafts like so:

has_drafts class_name: 'Draft'

Reasons for failure

It would be nice if save_draft could add to errors
or have an optional mode where it could raise an exception in development mode so I knew why something wasn't working :-(

Exception when destroying drafts

Looks like a small problem with checking for the JSON data type in PostgreSQL; causes draft_destroy to fail.

NoMethodError - undefined method `type' for nil:NilClass:
  <path>/draftsman/lib/draftsman/draft.rb:37:in `previous_changes_col_is_json?'
  <path>/draftsman/lib/draftsman/model.rb:243:in `block (2 levels) in draft_destroy'
  activerecord (4.2.0) lib/active_record/connection_adapters/abstract/database_statements.rb:211:in `transaction'
  activerecord (4.2.0) lib/active_record/transactions.rb:220:in `transaction'
  activerecord (4.2.0) lib/active_record/transactions.rb:277:in `transaction'
  <path>/draftsman/lib/draftsman/model.rb:231:in `block in draft_destroy'
  ...

I'll submit a pull request shortly that fixes this.

Fails miserably with foreign keys

Using Draftsman on a Rails 4.2 project, which adds foreign keys via the DB migrations.

So far, I've uncovered that calling draft_update to return a record back to its pre-draft state causes a PG::ForeignKeyViolation with Postgres.

I really need to get this repo running with Travis. (See #15.)

Stack too deep: Error when running `bundle exec rails c` in app including draftsman

Hey @chrisdpeters, I'm not sure how to fix this one, and thought you could help.

My application includes the draftsman gem, and I'm running Bundler version 1.9.9. When I run bundle exec rails c in my application, I get the following error:

Bundler is using a binstub that was created for a different gem.
This is deprecated, in future versions you may need to `bundle binstub railties` to work around a system/bundle conflict.

However, this shows up thousands of times, until I eventually get a "stack too deep" error, and the rails console never launches:

<path to rbenv>/.rbenv/versions/2.2.1/lib/ruby/gems/2.2.0/gems/bundler-1.9.9/lib/bundler/spec_set.rb:111:in `each': stack level too deep (SystemStackError)
    from <path to rbenv>/.rbenv/versions/2.2.1/lib/ruby/gems/2.2.0/gems/bundler-1.9.9/lib/bundler/spec_set.rb:111:in `find'
    from <path to rbenv>/.rbenv/versions/2.2.1/lib/ruby/gems/2.2.0/gems/bundler-1.9.9/lib/bundler/spec_set.rb:111:in `sorted'
    from <path to rbenv>/.rbenv/versions/2.2.1/lib/ruby/gems/2.2.0/gems/bundler-1.9.9/lib/bundler/rubygems_integration.rb:315:in `find'
    from <path to rbenv>/.rbenv/versions/2.2.1/lib/ruby/gems/2.2.0/gems/bundler-1.9.9/lib/bundler/rubygems_integration.rb:315:in `block in replace_bin_path'
    from <local path to draftsman>/draftsman/bin/rails:16:in `<top (required)>'
    from <local path to draftsman>/draftsman/bin/rails:16:in `load'
    from <local path to draftsman>/draftsman/bin/rails:16:in `<top (required)>'
    from <local path to draftsman>/draftsman/bin/rails:16:in `load'
     ... 10065 levels...
    from <local path to draftsman>/draftsman/bin/rails:16:in `load'
    from <local path to draftsman>/draftsman/bin/rails:16:in `<top (required)>'
    from <path to rbenv>/.rbenv/versions/2.2.1/lib/ruby/gems/2.2.0/bin/rails:23:in `load'
    from <path to rbenv>/.rbenv/versions/2.2.1/lib/ruby/gems/2.2.0/bin/rails:23:in `<main>'

I can run rails c locally just fine, but our staging and production environments require calling bundle exec, so they fail when trying to run. I'm a little out of my depth here, and not quite sure how these binstubs are used. This may fail for you too, on Bundler 1.9.9. Not sure if something changed in that release.

Thanks!

Integrating with acts_as_list

Hey Chris,

This is more question/discussion than issue, in case you want to tag as such.

In our application, we're using the acts_as_list gem to organize a few sortable objects. If you're not familiar, acts_as_list essentially handles ordering items based on scope.

acts_as_list writes directly to the given model's table when updating the position column, which (in our case) causes it to bypass draftsman.

I was curious if you had any experience with acts_as_list, and if you had any clever integration methods. I understand the burden isn't really on draftsman -- acts_as_list would probably have to change to allow simple integration. I still thought I'd ask, just in case.

As a note, we may be moving away from acts_as_list, since it's causing a number of other little problems for us. If so, this is totally a non-issue.

Thanks!

Fix "open-ended dependency on rake" warning on gem build

$ gem build draftsman.gemspec

WARNING:  open-ended dependency on rake (>= 0, development) is not recommended
  if rake is semantically versioned, use:
    add_development_dependency 'rake', '~> 0'
WARNING:  See http://guides.rubygems.org/specification-reference/ for help

Model attributes stored with JSON coder don't deserialize (via reify) correctly

This is a great gem! Even in the experimental state, it works incredibly well.

I found one issue when storing an attribute coded in JSON. When I reify from a draft, the attribute is returned as an encoded String rather than the expected JSON object.

As a brief example, assume I have a model with a settings attribute that looks something like this:

class Widget < ActiveRecord::Base
  store :settings, coder: JSON
end

In the database, the settings column is simply a text type field.

Assume I have a widget object and set its settings attribute to the following JSON object:
{"key1":"val1", "key2":"val2"}
If I run a draft_update, this will be saved into the drafts table correctly. Publishing also works as expected. However, reifying seems to return this object as a String rather than hash (or JSON object). widget.draft.reify.settings would return the following:
"{\"key1\":\"val1\", \"key2\":\"val2\"}"

I haven't yet found a workaround, but I'm still looking. If there's something simple you might recommend, let me know.

Thanks for all the development work you've put into draftsman!

Decide on changes for callbacks in response to new #save_draft method

I posed a question to @npafundi here in #47. I've since merged into master so I can work on some other features without an awful merge conflict.

Let's go ahead and have the discussion and track progress with callbacks in this issue.

Here is the question:

@npafundi I think that this is a good move for the API of this gem. It is a little confusing knowing when to call #draft_creation vs. #draft_update.

However, this could potentially affect how we do callbacks. If drafting behavior is moved into a single method, I'm not convinced that it would be clear to users of this gem when *_draft_creation vs. *_draft_update callbacks would be triggered. I think it's a little ambiguous: is before_draft_creation called when a draft record is being first created? Or is it when the item is created? See what I mean?

So I must ask, which of the callbacks are you using right now? Would you miss anything if I removed the ambiguous ones and created more general before_save_draft, after_save_draft, and around_save_draft callbacks?

Error when publishing a draft that has a polymorphic association if the association is nil

I have a Slide class which optionally belongs to an instance of a "slideable" class:

belongs_to :slideable, polymorphic: true

Line 79 of draft.rb assumes that calling my_item.slideable_type will always return a string that can be constantized to find the associated class.

my_item.send(association.foreign_key.sub('_id', '_type')).constantize

However, in my case, because I don't have slideable_type defined until I actually have a slideable associated which results in this error:

NoMethodError: undefined method `constantize' for nil:NilClass

I think I can work around this by having a default value for slideable_type set in Postgres but it seems like it might be fairly easy to add some logic to check if the polymorphic type is defined/valid when trying to determine the association class.

[question] Having trouble running specs

Hey Chris,

I forked draftsman to add a small piece of functionality, and wanted to send you a pull request. However, I'm having trouble getting the specs to run, and didn't want to send a pull request without running (and adding) tests.

When I run RAILS_ENV=test rake db:migrate (or other db and spec tasks), I get:

rake aborted!
LoadError: cannot load such file -- rack/test
/<my path>/draftsman/spec/dummy/config/application.rb:4:in `<top (required)>'
/<my path>/draftsman/spec/dummy/Rakefile:5:in `<top (required)>'
LoadError: cannot load such file -- active_record/railtie
/<my path>/draftsman/spec/dummy/config/application.rb:4:in `<top (required)>'
/<my path>/draftsman/spec/dummy/Rakefile:5:in `<top (required)>'

I was following your listed steps in the "Contributing" section of the docs. Sorry if I'm missing something obvious here!

Thanks again!
Nick

draft_update updates published object and draft

I thought I could use Draftsman to save and update drafts of objects without also saving to the original object, but it looks like the draft_update method updates and saves both the original object and the draft.

Right now I'm saving an object (a post) as a draft until published, and I'm publishing it through a custom publish action in my controller. After publishing I'd like to be able to go back and create/update a draft of that published post without also updating the published post. I can't seem to find a method in the documentation that does this.

Is there a method that updates just the draft so that a published object isn't immediately updated when its draft is updated?

Problem with nested attributes

Given:

class Widget < ActiveRecord::Base
  accept_nested_attributes_for :cars, allow_destroy: true
  has_drafts ignore: [:slug]
end

class WidgetsController < InheritedResources::Base
  def update
    @widget.attributes = widget_params # Update nested attributes already here
    if @widget.draft_update
     # ok
    else
      update! # Inherited resources update without draft, raising exception if destroyed or create duplicates because they were created in above code
    end
  end
end

the problem - nested object are created or destroyed twice(raising not found on second destroy).

Does anyone faced with similar problem ?

License missing from gemspec

RubyGems.org doesn't report a license for your gem. This is because it is not specified in the gemspec of your last release.

via e.g.

  spec.license = 'MIT'
  # or
  spec.licenses = ['MIT', 'GPL-2']

Including a license in your gemspec is an easy way for rubygems.org and other tools to check how your gem is licensed. As you can imagine, scanning your repository for a LICENSE file or parsing the README, and then attempting to identify the license or licenses is much more difficult and more error prone. So, even for projects that already specify a license, including a license in your gemspec is a good practice. See, for example, how rubygems.org uses the gemspec to display the rails gem license.

There is even a License Finder gem to help companies/individuals ensure all gems they use meet their licensing needs. This tool depends on license information being available in the gemspec. This is an important enough issue that even Bundler now generates gems with a default 'MIT' license.

I hope you'll consider specifying a license in your gemspec. If not, please just close the issue with a nice message. In either case, I'll follow up. Thanks for your time!

Appendix:

If you need help choosing a license (sorry, I haven't checked your readme or looked for a license file), GitHub has created a license picker tool. Code without a license specified defaults to 'All rights reserved'-- denying others all rights to use of the code.
Here's a list of the license names I've found and their frequencies

p.s. In case you're wondering how I found you and why I made this issue, it's because I'm collecting stats on gems (I was originally looking for download data) and decided to collect license metadata,too, and make issues for gemspecs not specifying a license as a public service :). See the previous link or my blog post about this project for more information.

Skipped attributes aren't updated if a model has a draft

Not a big issue, but there's a saving inconsistency when a model has a draft vs. when it does not.

When a model doesn't have a draft and both skipped and non-skipped attributes are updated, the skipped attributes are updated on the model, while the non-skipped attributes are updated in a new "update" draft.

When a model has a draft and both skipped and non-skipped attributes are updated, the skipped attributes are not updated on the model, and the non-skipped attributes are updated in a new "update" draft.

I'd assume they're both supposed to do the same thing -- either the skipped attributes are always updated or never updated. It doesn't really matter to me which way it goes, but it seems like a consistent update or non-update would be best in these cases.

I will write a test case and patch for this under the assumption that models should always be updated, but if that's the wrong assumption, we can flip it around.

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.