Setup basic Revision History page

Sets up paper_trail tracking in important models

and add conference_id to versions for storing conference_id
of conference related objects  as metadata.This will help in querying
for versions related to conferences.

Fix issues with factories & event types initialization.Import paper_trail testing helpers
Fix bug: Creating an event creates a useless version with event update

Add revert and view changes features to revision history
This commit is contained in:
Nishanth Vijayan 2016-05-24 23:54:46 +05:30
parent 60877e9593
commit a45e537bac
49 changed files with 760 additions and 26 deletions

View file

@ -41,6 +41,7 @@
//= require osem-schedule
//= require osem-switch
//= require osem-bootstrap
//= require osem-revisionhistory
//= require osem-commercials
//= require unobtrusive_flash
//= require unobtrusive_flash_bootstrap

View file

@ -7,6 +7,11 @@ $(function () {
pagingType: 'full_numbers',
"lengthMenu": [[25, 50, 100, -1], [25, 50, 100, "All"]]
});
$('#versionstable').DataTable({
pagingType: 'full_numbers',
order: [[ 0, 'desc' ]]
});
});
});

View file

@ -0,0 +1,10 @@
$(document).ready(function() {
$('.show-changeset').click(function(){
if ($(this).text() == 'View Changes'){
$(this).text('Hide Changes');
}else {
$(this).text('View Changes');
}
$('#changeset-' + this.id).toggle();
});
});

View file

@ -86,3 +86,7 @@ p.comment-body {
#proposal-info div dt, #proposal-info div dd {
display: inline-block;
}
.changeset{
display: none;
}

View file

@ -0,0 +1,58 @@
module Admin
class VersionsController < Admin::BaseController
skip_authorization_check
def index
authorize! :index, PaperTrail::Version.new(item_type: 'User')
conf_ids_for_organizer = current_user.is_admin? ? Conference.pluck(:id) : Conference.with_role(:organizer, current_user).pluck(:id)
@versions = PaperTrail::Version.where(["conference_id IN (?) OR item_type = 'User'", conf_ids_for_organizer])
end
def revert_attribute
@version = PaperTrail::Version.find(params[:id])
authorize! :revert_attribute, @version
if params[:attribute] && @version.changeset.reject{ |_, values| values[0].blank? && values[1].blank? }.keys.include?(params[:attribute])
if @version.item[params[:attribute]] == @version.changeset[params[:attribute]][0]
flash[:error] = 'The item is already in the state that you are trying to revert it back to'
else
@version.item[params[:attribute]] = @version.changeset[params[:attribute]][0]
if @version.item.save
flash[:notice] = 'The selected change was successfully reverted'
else
flash[:error] = "An error prohibited this change from being reverted: #{@version.item.errors.full_messages.join('. ')}."
end
end
else
flash[:error] = 'Revert failed. Attribute missing or invalid'
end
redirect_to admin_revision_history_path
end
def revert_object
@version = PaperTrail::Version.find(params[:id])
authorize! :revert_object, @version
if @version.event != 'create'
if @version.reify.save
flash[:notice] = 'The selected change was successfully reverted'
else
flash[:error] = "An error prohibited this change from being reverted: #{@version.reify.errors.full_messages.join('. ')}."
end
elsif @version.event == 'create' && @version.item
# if @version represets a create event and is not currently deleted
@version.item.destroy
flash[:notice] = 'The selected change was successfully reverted'
else
flash[:error] = 'The item is already in the state that you are trying to revert it back to'
end
redirect_to admin_revision_history_path
end
end
end

View file

@ -370,4 +370,125 @@ module ApplicationHelper
def selected_scheduled?(schedule)
(schedule == @selected_schedule) ? 'Yes' : 'No'
end
# Recieves a PaperTrail::Version object
# Outputs the list of attributes that were changed in the version (ignoring changes from one blank value to another)
# Eg: If version.changeset = '{"title"=>[nil, "Premium"], "description"=>[nil, "Premium = Super cool"], "conference_id"=>[nil, 3]}'
# Output will be 'title, description and conference'
def updated_attributes(version)
version.changeset.
reject{ |_, values| values[0].blank? && values[1].blank? }.
keys.map{ |key| key.gsub('_id', '').tr('_', ' ')}.join(', ').
reverse.sub(',', ' dna ').reverse
end
def change_creator_link(user_id)
user = User.find_by(id: user_id)
if user
link_to user.name, admin_user_path(id: user_id)
else
'Someone (probably via the console)'
end
end
# Recieves a PaperTrail::Version object
# Returns object in its current state if its alive
# Returns object as it was before version's change(unless its a create event's version)
# Else Returns object as it was after version's change
def get_version_object(version)
version.item || version.reify || version.next.reify
end
def event_change_description(version)
case
when version.event == 'create' then 'submitted new'
when version.changeset['state']
case version.changeset['state'][1]
when 'unconfirmed' then 'accepted'
when 'withdrawn' then 'withdrew'
when 'canceled', 'rejected', 'confirmed' then version.changeset['state'][1]
when 'new' then 'resubmitted'
end
when version.changeset['start_time'] && version.changeset['start_time'][0].nil?
'scheduled'
when version.changeset['start_time'] && version.changeset['start_time'][1].nil?
'unscheduled'
else
"updated #{updated_attributes(version)} of"
end
end
def users_role_change_description(version)
version.event == 'create' ? 'added' : 'removed'
end
def subscription_change_description(version)
user = get_version_object(version).user
user_name = user.name unless user.id.to_s == version.whodunnit
version.event == 'create' ? "subscribed #{user_name} to" : "unsubscribed #{user_name} from"
end
def registration_change_description(version)
if version.item_type == 'Registration'
user = get_version_object(version).user
else
registration_id = get_version_object(version).registration_id
registration_last_version = PaperTrail::Version.where(item_type: 'Registration', item_id: registration_id).last
user = get_version_object(registration_last_version).user
end
if user.id.to_s == version.whodunnit
case version.event
when 'create' then 'registered to'
when 'update' then "updated #{updated_attributes(version)} of the registration for"
when 'destroy' then 'unregistered from'
end
else
case version.event
when 'create' then "registered #{user.name} to"
when 'update' then "updated #{updated_attributes(version)} of #{user.name}'s registration for"
when 'destroy' then "unregistered #{user.name} from"
end
end
end
def comment_change_description(version)
user = get_version_object(version).user
if version.event == 'create'
version.previous.nil? ? 'commented on' : "re-added #{user.name}'s comment on"
else
"deleted #{user.name}'s comment on"
end
end
def vote_change_description(version)
user = get_version_object(version).user
if version.event == 'create'
version.previous.nil? ? 'voted on' : "re-added #{user.name}'s vote on"
elsif version.event == 'update'
"updated #{user.name}'s vote on"
else
"deleted #{user.name}'s vote on"
end
end
def user_change_description(version)
if version.event == 'create'
change_creator_link(version.item_id) + ' signed up'
elsif version.event == 'update'
if version.changeset.keys.include?('reset_password_sent_at')
'Someone requested password reset of'
elsif version.changeset.keys.include?('confirmed_at') && version.changeset['confirmed_at'][0].nil?
(version.whodunnit.nil? ? change_creator_link(version.item_id) : change_creator_link(version.whodunnit)) + ' confirmed account of'
elsif version.changeset.keys.include?('confirmed_at') && version.changeset['confirmed_at'][1].nil?
change_creator_link(version.whodunnit) + ' unconfirmed account of'
else
change_creator_link(version.whodunnit) + " updated #{updated_attributes(version)} of"
end
end
end
end

View file

@ -114,6 +114,14 @@ class Ability
# for admins
can :manage, :all if user.is_admin
cannot :revert_object, PaperTrail::Version do |version|
(version.event == 'create' && %w(Conference User Event).include?(version.item_type))
end
cannot :revert_attribute, PaperTrail::Version do |version|
version.event != 'update' || version.item.nil?
end
cannot :destroy, Program
# Do not delete venue, when there are rooms being used
cannot :destroy, Venue do |venue|
@ -168,6 +176,10 @@ class Ability
can [:edit, :update, :toggle_user], Role do |role|
role.resource_type == 'Conference' && (conf_ids_for_organizer.include? role.resource_id)
end
can [:index, :revert_object, :revert_attribute], PaperTrail::Version do |version|
version.item_type == 'User' || (conf_ids_for_organizer.include? version.conference_id)
end
end
def signed_in_with_cfp_role(user)

View file

@ -4,6 +4,8 @@ class Campaign < ActiveRecord::Base
has_many :targets, dependent: :nullify
belongs_to :conference
has_paper_trail ignore: [:updated_at], meta: { conference_id: :conference_id }
##
# Returns the utm parameters formatted as url.
#

View file

@ -1,6 +1,7 @@
# cannot delete program if there are events submitted
class Cfp < ActiveRecord::Base
has_paper_trail ignore: [:updated_at], meta: { conference_id: :conference_id }
belongs_to :program
validates :program_id, presence: true
@ -68,4 +69,8 @@ class Cfp < ActiveRecord::Base
errors.
add(:start_date, "can't be after the end date") if start_date && end_date && start_date > end_date
end
def conference_id
program.conference_id
end
end

View file

@ -13,6 +13,8 @@ class Comment < ActiveRecord::Base
# NOTE: Comments belong to a user
belongs_to :user
has_paper_trail on: [:create, :destroy], meta: { conference_id: :conference_id }
# Helper class method that allows you to build a comment
# by passing a commentable object, a user_id, and comment text
# example in readme
@ -58,4 +60,8 @@ class Comment < ActiveRecord::Base
def send_notification
EventCommentMailJob.perform_later(self)
end
def conference_id
commentable.program.conference_id
end
end

View file

@ -3,6 +3,8 @@ class Commercial < ActiveRecord::Base
belongs_to :commercialable, polymorphic: true
has_paper_trail ignore: [:updated_at], meta: { conference_id: :conference_id }
validates :url, presence: true
validates :url, format: URI::regexp(%w(http https))
@ -41,4 +43,12 @@ class Commercial < ActiveRecord::Base
speakerdeck
)
end
def conference_id
case commercialable_type
when 'Conference' then commercialable_id
when 'Event' then Event.find(commercialable_id).program.conference_id
when 'Venue' then Venue.find(commercialable_id).conference_id
end
end
end

View file

@ -9,7 +9,7 @@ class Conference < ActiveRecord::Base
default_scope { order('start_date DESC') }
has_paper_trail
has_paper_trail ignore: [:updated_at, :guid, :revision, :events_per_week], meta: { conference_id: :id }
has_and_belongs_to_many :questions

View file

@ -1,4 +1,6 @@
class Contact < ActiveRecord::Base
has_paper_trail on: [:update], ignore: [:updated_at], meta: { conference_id: :conference_id }
belongs_to :conference
validates :conference, presence: true

View file

@ -2,7 +2,10 @@ class DifficultyLevel < ActiveRecord::Base
belongs_to :program
has_many :events, dependent: :nullify
has_paper_trail ignore: [:updated_at], meta: { conference_id: :conference_id }
validates :title, presence: true
validates :color, format: /\A#[0-9A-F]{6}\z/
before_validation :capitalize_color
@ -12,4 +15,8 @@ class DifficultyLevel < ActiveRecord::Base
def capitalize_color
self.color = color.upcase if color.present?
end
def conference_id
program.conference_id
end
end

View file

@ -1,6 +1,8 @@
class EmailSettings < ActiveRecord::Base
belongs_to :conference
has_paper_trail on: [:update], ignore: [:updated_at], meta: { conference_id: :conference_id }
def get_values(conference, user, event = nil)
h = {
'email' => user.email,

View file

@ -1,6 +1,6 @@
class Event < ActiveRecord::Base
include ActiveRecord::Transitions
has_paper_trail
has_paper_trail on: [:create, :update], ignore: [:updated_at, :guid, :week], meta: { conference_id: :conference_id }
acts_as_commentable
@ -280,7 +280,9 @@ class Event < ActiveRecord::Base
def set_week
self.week = created_at.strftime('%W')
save!
self.without_versioning do
self.save!
end
end
def before_end_of_conference
@ -288,4 +290,8 @@ class Event < ActiveRecord::Base
add(:created_at, "can't be after the conference end date!") if program.conference && program.conference.end_date &&
(Date.today > program.conference.end_date)
end
def conference_id
program.conference_id
end
end

View file

@ -2,6 +2,8 @@ class EventType < ActiveRecord::Base
belongs_to :program
has_many :events, dependent: :restrict_with_error
has_paper_trail meta: { conference_id: :conference_id }
validates :title, presence: true
validates :length, numericality: {greater_than: 0}
validates :minimum_abstract_length, presence: true
@ -28,4 +30,8 @@ class EventType < ActiveRecord::Base
def capitalize_color
self.color = color.upcase if color.present?
end
def conference_id
program.conference_id
end
end

View file

@ -4,9 +4,17 @@ class EventsRegistration < ActiveRecord::Base
has_one :user, through: :registration
has_paper_trail meta: { conference_id: :conference_id }
delegate :name, to: :registration
delegate :email, to: :registration
validates :event, :registration, presence: true
validates :event, uniqueness: { scope: :registration }
private
def conference_id
registration.conference_id
end
end

View file

@ -1,6 +1,8 @@
class Lodging < ActiveRecord::Base
belongs_to :conference
has_paper_trail ignore: [:updated_at], meta: { conference_id: :conference_id }
validates :name, presence: true
mount_uploader :picture, PictureUploader, mount_on: :photo_file_name

View file

@ -1,6 +1,8 @@
# cannot delete program if there are events submitted
class Program < ActiveRecord::Base
has_paper_trail on: [:update], ignore: [:updated_at], meta: { conference_id: :conference_id }
belongs_to :conference
has_one :cfp, dependent: :destroy
@ -53,8 +55,8 @@ class Program < ActiveRecord::Base
validate :voting_start_date_before_end_date
validate :voting_dates_exist
before_create :create_event_types
before_create :create_difficulty_levels
after_create :create_event_types
after_create :create_difficulty_levels
validate :check_languages_format
# Returns all event_schedules for the selected schedule ordered by start_time
@ -157,12 +159,12 @@ class Program < ActiveRecord::Base
# Creates default EventTypes for this Conference. Used as before_create.
#
def create_event_types
event_types << EventType.create(title: 'Talk', length: 30, color: '#FF0000', description: 'Presentation in lecture format',
minimum_abstract_length: 0,
maximum_abstract_length: 500)
event_types << EventType.create(title: 'Workshop', length: 60, color: '#0000FF', description: 'Interactive hands-on practice',
minimum_abstract_length: 0,
maximum_abstract_length: 500)
EventType.create(title: 'Talk', length: 30, color: '#FF0000', description: 'Presentation in lecture format',
minimum_abstract_length: 0,
maximum_abstract_length: 500, program_id: self.id)
EventType.create(title: 'Workshop', length: 60, color: '#0000FF', description: 'Interactive hands-on practice',
minimum_abstract_length: 0,
maximum_abstract_length: 500, program_id: self.id)
true
end
@ -170,15 +172,15 @@ class Program < ActiveRecord::Base
# Creates default DifficultyLevels for this Conference. Used as before_create.
#
def create_difficulty_levels
difficulty_levels << DifficultyLevel.create(title: 'Easy',
description: 'Events are understandable for everyone without knowledge of the topic.',
color: '#70EF69')
difficulty_levels << DifficultyLevel.create(title: 'Medium',
description: 'Events require a basic understanding of the topic.',
color: '#EEEF69')
difficulty_levels << DifficultyLevel.create(title: 'Hard',
description: 'Events require expert knowledge of the topic.',
color: '#EF6E69')
DifficultyLevel.create(title: 'Easy',
description: 'Events are understandable for everyone without knowledge of the topic.',
color: '#70EF69', program_id: self.id)
DifficultyLevel.create(title: 'Medium',
description: 'Events require a basic understanding of the topic.',
color: '#EEEF69', program_id: self.id)
DifficultyLevel.create(title: 'Hard',
description: 'Events require expert knowledge of the topic.',
color: '#EF6E69', program_id: self.id)
true
end

View file

@ -7,7 +7,9 @@ class Registration < ActiveRecord::Base
has_and_belongs_to_many :vchoices
has_many :events_registrations
has_many :events, through: :events_registrations
has_many :events, through: :events_registrations, dependent: :destroy
has_paper_trail ignore: [:updated_at, :week], meta: { conference_id: :conference_id }
accepts_nested_attributes_for :user
accepts_nested_attributes_for :qanswers
@ -75,7 +77,9 @@ class Registration < ActiveRecord::Base
def set_week
self.week = created_at.strftime('%W')
save!
without_versioning do
save!
end
end
def registration_limit_not_exceed

View file

@ -1,6 +1,8 @@
class RegistrationPeriod < ActiveRecord::Base
belongs_to :conference
has_paper_trail ignore: [:updated_at], meta: { conference_id: :conference_id }
validates :start_date, :end_date, presence: true
validate :before_end_of_conference
validate :start_date_before_end_date

View file

@ -1,6 +1,10 @@
class Role < ActiveRecord::Base
belongs_to :resource, polymorphic: true
has_and_belongs_to_many :users, join_table: :users_roles
has_many :users_roles
has_many :users, through: :users_roles
has_paper_trail on: [:update], only: [:name, :description], meta: { conference_id: :resource_id }
before_destroy :cancel
scopify

View file

@ -2,6 +2,8 @@ class Room < ActiveRecord::Base
belongs_to :venue
has_many :event_schedules, dependent: :nullify
has_paper_trail ignore: [:guid], meta: { conference_id: :conference_id }
before_create :generate_guid
validates :name, :venue_id, presence: true
@ -14,4 +16,8 @@ class Room < ActiveRecord::Base
guid = SecureRandom.urlsafe_base64
self.guid = guid
end
def conference_id
venue.conference_id
end
end

View file

@ -1,3 +1,5 @@
class Splashpage < ActiveRecord::Base
belongs_to :conference
has_paper_trail ignore: [:updated_at], meta: { conference_id: :conference_id }
end

View file

@ -2,6 +2,8 @@ class Sponsor < ActiveRecord::Base
belongs_to :sponsorship_level
belongs_to :conference
has_paper_trail ignore: [:updated_at], meta: { conference_id: :conference_id }
mount_uploader :picture, PictureUploader, mount_on: :logo_file_name
validates_presence_of :name, :website_url, :sponsorship_level

View file

@ -3,4 +3,6 @@ class SponsorshipLevel < ActiveRecord::Base
belongs_to :conference
acts_as_list scope: :conference
has_many :sponsors
has_paper_trail ignore: [:updated_at], meta: { conference_id: :conference_id }
end

View file

@ -3,5 +3,7 @@ class Subscription < ActiveRecord::Base
belongs_to :conference
belongs_to :user
has_paper_trail on: [:create, :destroy], ignore: [:updated_at], meta: { conference_id: :conference_id }
validates_uniqueness_of :user_id, scope: :conference_id, message: 'already subscribed!'
end

View file

@ -3,6 +3,8 @@ class Target < ActiveRecord::Base
default_scope { order('due_date ASC') }
has_paper_trail ignore: [:updated_at], meta: { conference_id: :conference_id }
def self.units
{
registrations: 'Registration',

View file

@ -3,6 +3,8 @@ class Ticket < ActiveRecord::Base
has_many :ticket_purchases, dependent: :destroy
has_many :buyers, -> { distinct }, through: :ticket_purchases, source: :user
has_paper_trail meta: { conference_id: :conference_id }
monetize :price_cents, with_model_currency: :price_currency
# This validation is for the sake of simplicity.

View file

@ -2,6 +2,8 @@ class Track < ActiveRecord::Base
belongs_to :program
has_many :events, dependent: :nullify
has_paper_trail only: [:name, :description, :color], meta: { conference_id: :conference_id }
before_create :generate_guid
validates :name, presence: true
validates :color, format: /\A#[0-9A-F]{6}\z/
@ -21,4 +23,8 @@ class Track < ActiveRecord::Base
def capitalize_color
self.color = color.upcase if color.present?
end
def conference_id
program.conference_id
end
end

View file

@ -6,6 +6,12 @@ end
class User < ActiveRecord::Base
rolify
has_many :users_roles
has_many :roles, through: :users_roles, dependent: :destroy
has_paper_trail on: [:create, :update], ignore: [:sign_in_count, :remember_created_at, :current_sign_in_at, :last_sign_in_at, :current_sign_in_ip, :last_sign_in_ip, :unconfirmed_email,
:avatar_content_type, :avatar_file_size, :avatar_updated_at, :updated_at, :confirmation_sent_at, :confirmation_token, :reset_password_token]
include Gravtastic
gravtastic size: 32
@ -72,7 +78,7 @@ class User < ActiveRecord::Base
end
def name
self[:name] || username
self[:name].blank? ? username : self[:name]
end
##

12
app/models/users_role.rb Normal file
View file

@ -0,0 +1,12 @@
class UsersRole < ActiveRecord::Base
belongs_to :role
belongs_to :user
has_paper_trail on: [:create, :destroy], meta: { conference_id: :conference_id }
private
def conference_id
role.resource_id
end
end

View file

@ -4,6 +4,8 @@ class Venue < ActiveRecord::Base
has_many :rooms, dependent: :destroy
before_create :generate_guid
has_paper_trail ignore: [:updated_at, :guid], meta: { conference_id: :conference_id }
accepts_nested_attributes_for :commercial, allow_destroy: true
validates :name, :street, :city, :country, presence: true

View file

@ -1,5 +1,14 @@
class Vote < ActiveRecord::Base
belongs_to :user
belongs_to :event
has_paper_trail ignore: [:updated_at], meta: { conference_id: :conference_id }
delegate :name, to: :user
private
def conference_id
event.program.conference_id
end
end

View file

@ -0,0 +1,28 @@
.col-md-10.col-md-offset-1.changeset{id: "changeset-#{version.id}"}
%br
%br
%table.table.table-bordered.table-hover
%thead
%tr
%th Updated Attribute
- if version.event != 'create'
%th Previous Value
- if version.event != 'destroy'
%th New Value
- if can? :revert_attribute, version
%th Action
%tbody
- if version.event!= 'destroy'
- version.changeset.reject{ |_, values| values[0].blank? && values[1].blank? }.each do |attribute, values|
%tr
%td= attribute
- if version.event != 'create'
%td= values[0].blank? ? '-' : values[0]
%td= values[1].blank? ? '-' : values[1]
- if can? :revert_attribute, version
%td= link_to 'Revert', admin_revision_history_revert_attribute_path(id: version.id, attribute: attribute), class: 'btn btn-sm btn-primary', data: { confirm: "Are you sure you want to revert #{attribute}?" }
- else
- version.reify.attributes.each do |attribute, value|
%tr
%td= attribute
%td= value.blank? ? '-' : value

View file

@ -0,0 +1,158 @@
- if version.item_type == 'UsersRole'
- users_role = get_version_object(version)
= 'role'
= link_to users_role.role.name, admin_conference_role_path(conference_id: Conference.find(version.conference_id).short_title, id: users_role.role.name)
= version.event == 'create' ? 'to' : 'from'
= 'user'
= link_to users_role.user.name, admin_user_path(id: users_role.user.id)
- elsif version.item_type == 'Subscription' || version.item_type == 'Registration'
= 'conference'
= link_to Conference.find(version.conference_id).title,
admin_conference_registrations_path(conference_id: Conference.find(version.conference_id).short_title)
- elsif version.item_type == 'Commercial'
- commercial_last_version = get_version_object(PaperTrail::Version.where(item_type: version.item_type, item_id: version.item_id).last)
- commercialable_last_version = get_version_object(PaperTrail::Version.where(item_type: commercial_last_version.commercialable_type,
item_id: commercial_last_version.commercialable_id).last)
- case commercial_last_version.commercialable_type
- when 'Event'
= link_to 'commercial',
edit_admin_conference_program_event_path(conference_id: Conference.find(version.conference_id).short_title,
id: commercialable_last_version.id, anchor: 'commercials-content')
= "in event #{commercialable_last_version.title}"
- when 'Venue'
- if Venue.find_by(id: commercialable_last_version.id)
= link_to 'commercial',
edit_admin_conference_program_event_path(conference_id: Conference.find(version.conference_id).short_title,
id: commercialable_last_version.id, anchor: 'commercials-content')
- else
= 'commercial'
= "in venue #{commercialable_last_version.name}"
- when 'Conference'
= link_to 'commercial',
admin_conference_commercials_path(conference_id: Conference.find(version.conference_id).short_title)
- elsif %w(EventsRegistration Comment Vote).include?(version.item_type)
= 'event'
- event_id = get_version_object(version).try(:event_id) || get_version_object(version).try(:commentable_id)
= link_to Event.find(event_id).title,
admin_conference_program_event_path(conference_id: Conference.find(version.conference_id).short_title, id: event_id)
- elsif version.item_type =='Target'
= 'target'
- if version.item
= link_to Target.find(version.item_id).to_s,
admin_conference_targets_path(conference_id: Conference.find(version.conference_id).short_title)
- else
= PaperTrail::Version.where(item_type: 'Target', item_id: version.item_id).last.reify.to_s
- elsif version.item
- case version.item_type
- when 'Conference'
= 'conference'
= link_to Conference.find(version.conference_id).title,
edit_admin_conference_path(id: Conference.find(version.conference_id).short_title)
- when 'RegistrationPeriod'
= link_to 'registration period',
admin_conference_registration_period_path(conference_id: Conference.find(version.conference_id).short_title)
- when 'Contact'
= link_to 'contact details',
edit_admin_conference_contact_path(conference_id: Conference.find(version.conference_id).short_title)
- when 'Program'
= link_to 'program',
admin_conference_program_path(conference_id: Conference.find(version.conference_id).short_title)
- when 'Cfp'
= link_to 'cfp',
admin_conference_program_cfp_path(conference_id: Conference.find(version.conference_id).short_title)
- when 'Track'
= 'track'
= link_to Track.find(version.item_id).name,
admin_conference_program_track_path(conference_id: Conference.find(version.conference_id).short_title, id: version.item_id)
- when 'Event'
= 'event'
= link_to Event.find(version.item_id).title,
admin_conference_program_event_path(conference_id: Conference.find(version.conference_id).short_title, id: version.item_id)
- when 'EventType'
= 'event type'
= link_to EventType.find(version.item_id).title,
admin_conference_program_event_types_path(conference_id: Conference.find(version.conference_id).short_title)
- when 'Role'
= 'role'
= link_to Role.find(version.item_id).name,
admin_conference_role_path(conference_id: Conference.find(version.conference_id).short_title, id: version.item.name)
- when 'Venue'
= 'venue'
= link_to Venue.find(version.item_id).name,
admin_conference_venue_path(conference_id: Conference.find(version.conference_id).short_title)
- when 'Lodging'
= 'lodging'
= link_to Lodging.find(version.item_id).name,
admin_conference_lodgings_path(conference_id: Conference.find(version.conference_id).short_title)
- when 'Room'
= 'room'
= link_to Room.find(version.item_id).name,
admin_conference_venue_rooms_path(conference_id: Conference.find(version.conference_id).short_title)
- when 'Sponsor'
= 'sponsor'
= link_to Sponsor.find(version.item_id).name,
admin_conference_sponsors_path(conference_id: Conference.find(version.conference_id).short_title)
- when 'SponsorshipLevel'
= 'sponsorship level'
= link_to SponsorshipLevel.find(version.item_id).title,
admin_conference_sponsorship_levels_path(conference_id: Conference.find(version.conference_id).short_title)
- when 'Ticket'
= 'ticket'
= link_to Ticket.find(version.item_id).title,
admin_conference_ticket_path(conference_id: Conference.find(version.conference_id).short_title, id: version.item_id)
- when 'Campaign'
= 'campaign'
= link_to Campaign.find(version.item_id).name,
admin_conference_campaigns_path(conference_id: Conference.find(version.conference_id).short_title)
- when 'DifficultyLevel'
= 'difficulty level'
= link_to DifficultyLevel.find(version.item_id).title,
admin_conference_program_difficulty_level_path(conference_id: Conference.find(version.conference_id).short_title, id: version.item_id)
- when 'Splashpage'
= link_to 'splashpage',
admin_conference_splashpage_path(conference_id: Conference.find(version.conference_id).short_title)
- when 'EmailSettings'
= link_to 'email settings',
admin_conference_emails_path(conference_id: Conference.find(version.conference_id).short_title)
- when 'User'
- if version.event == 'update'
= 'user'
= link_to User.find(version.item_id).name, admin_user_path(id: version.item_id)
- else
= version.item_type.underscore.tr('_', ' ')
/ The last deleted version's name/title is used to describe all the changes in object
- last_deleted_version = PaperTrail::Version.where(item_type: version.item_type, item_id: version.item_id).last.reify
= last_deleted_version.try(:title) || last_deleted_version.try(:name)
- unless %w(Conference Subscription Registration User).include?(version.item_type)
= "in conference"
= link_to Conference.find(version.conference_id).short_title,
edit_admin_conference_path(id: Conference.find(version.conference_id).short_title)

View file

@ -0,0 +1,77 @@
.row
.col-md-12
.page-header
%h1 Revision History
%p.text-muted
Log of changes made to conferences and associated resources
%table.table.table-striped.table-bordered.table-hover#versionstable
%thead
%td ID
%td Description
%td Actions
%tbody
- @versions.each do |version|
%tr
%td.col-md-1
= version.id
%td.col-md-9
%p
= change_creator_link(version.whodunnit) unless version.item_type == 'User'
- case version.item_type
- when 'Event'
= event_change_description(version)
-when 'UsersRole'
= users_role_change_description(version)
- when 'Subscription'
= subscription_change_description(version)
- when 'Registration', 'EventsRegistration'
= registration_change_description(version)
- when 'Comment'
= comment_change_description(version)
- when 'Vote'
= vote_change_description(version)
- when 'User'
= user_change_description(version)
- else
- if version.event == 'create'
= 'created new'
- elsif version.event == 'update'
= "updated #{updated_attributes(version)} of"
- else
= 'deleted'
= render partial: 'object_desc_and_link', locals: { version: version }
%small.text-muted
= distance_of_time_in_words(Time.now,version.created_at) + ' ago'
%br
= "(#{version.created_at.strftime('%B %-d, %Y %H:%M')})"
%br
= render partial: 'object_changes', locals: { version: version }
%td.col-md-2
.btn-group{role: 'group'}
%a.btn.btn-success.btn-sm.show-changeset{id: version.id} View Changes
- if can? :revert_object, version
%button.btn.btn-default.dropdown-toggle.btn-sm.btn-primary{'data-toggle' => 'dropdown', type: 'button'}
Revert
%span.caret
%ul.dropdown-menu
%li= link_to 'All Changes', admin_revision_history_revert_object_path(id: version.id), data: { confirm: 'Are you sure you want to revert this change?' }
- if can? :revert_attribute, version
%li.divider{role: 'separator'}
- version.changeset.reject{ |_, values| values[0].blank? && values[1].blank? }.each do |attribute, values|
%li= link_to attribute, admin_revision_history_revert_attribute_path(id: version.id, attribute: attribute), data: { confirm: "Are you sure you want to revert #{attribute}?" }
- else
%button.btn.btn-sm.btn-primary.disabled Revert

View file

@ -27,3 +27,8 @@
= link_to(admin_users_path) do
%span.fa.fa-user
Users
- if can? :index, PaperTrail::Version.new(item_type: 'User')
%li
= link_to(admin_revision_history_path) do
%span.fa.fa-history
Revision History

View file

@ -44,3 +44,8 @@
= link_to(admin_users_path) do
%span.fa.fa-user
Users
- if can? :index, PaperTrail::Version.new(item_type: 'User')
%li
= link_to(admin_revision_history_path) do
%span.fa.fa-history
Revision History

View file

@ -91,6 +91,10 @@ Osem::Application.routes.draw do
end
end
end
get '/revision_history' => 'versions#index'
get '/revision_history/:id/revert_object' => 'versions#revert_object', as: 'revision_history_revert_object'
get '/revision_history/:id/revert_attribute' => 'versions#revert_attribute', as: 'revision_history_revert_attribute'
end
resources :conference, only: [:index, :show] do

View file

@ -0,0 +1,5 @@
class AddConferenceIdToVersions < ActiveRecord::Migration
def change
add_column :versions, :conference_id, :integer
end
end

View file

@ -0,0 +1,5 @@
class AddIdToUsersRoles < ActiveRecord::Migration
def change
add_column :users_roles, :id, :primary_key
end
end

View file

@ -483,7 +483,7 @@ ActiveRecord::Schema.define(version: 20160704092023) do
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
add_index "users", ["username"], name: "index_users_on_username", unique: true
create_table "users_roles", id: false, force: :cascade do |t|
create_table "users_roles", force: :cascade do |t|
t.integer "role_id"
t.integer "user_id"
end
@ -529,6 +529,7 @@ ActiveRecord::Schema.define(version: 20160704092023) do
t.text "object"
t.text "object_changes"
t.datetime "created_at"
t.integer "conference_id"
end
add_index "versions", ["item_type", "item_id"], name: "index_versions_on_item_type_and_item_id"

View file

@ -0,0 +1,101 @@
require 'spec_helper'
describe Admin::VersionsController do
let!(:conference) { create(:conference, short_title: 'exampletitle', description: 'Example Description') }
let(:admin) { create(:admin) }
with_versioning do
describe 'GET #revert' do
before :each do
sign_in admin
end
it 'reverts all changes for update actions' do
conference.update_attributes(short_title: 'testtitle', description: 'Some random text')
get :revert_object, id: PaperTrail::Version.last.id
conference.reload
expect(conference.short_title).to eq 'exampletitle'
expect(conference.description).to eq 'Example Description'
end
it 'shows correct flash on trying to revert create event of a deleted object' do
creation_version_id = conference.program.event_types.first.versions.first.id
conference.program.event_types.first.destroy
get :revert_object, id: creation_version_id
expect(flash[:error]).to match('The item is already in the state that you are trying to revert it back to')
end
it 'reverting deletion of object creates it again' do
conference.program.event_types.first.destroy
event_types_count = conference.program.event_types.count
get :revert_object, id: PaperTrail::Version.last.id
conference.reload
expect(PaperTrail::Version.last.event).to eq 'create'
expect(conference.program.event_types.count).to eq(event_types_count + 1)
end
it 'reverting creation of object deletes it ' do
create(:lodging, conference: conference)
get :revert_object, id: PaperTrail::Version.last.id
expect(PaperTrail::Version.last.event).to eq 'destroy'
expect(Lodging.count).to eq 0
end
it 'reverting creation of conference is not permitted' do
conference_count_before = Conference.count
get :revert_object, id: conference.versions.first.id
expect(flash[:alert]).to eq 'You are not authorized to access this page.'
expect(Conference.count).to eq(conference_count_before)
end
end
describe 'GET #revert_attribute' do
before :each do
sign_in admin
end
it 'reverts specified change for update actions' do
conference.update_attributes(short_title: 'testtitle', description: 'Some random text')
get :revert_attribute, id: PaperTrail::Version.last.id, attribute: 'short_title'
conference.reload
expect(conference.short_title).to eq 'exampletitle'
expect(conference.description).to eq 'Some random text'
end
it 'shows correct flash on trying to revert to the current state' do
conference.update_attributes(short_title: 'testtitle', description: 'Some random text')
conference.update_attributes(short_title: 'exampletitle')
get :revert_attribute, id: PaperTrail::Version.all[-2].id, attribute: 'short_title'
expect(flash[:error]).to match('The item is already in the state that you are trying to revert it back to')
expect(conference.short_title).to eq 'exampletitle'
end
it 'fails on trying to revert deleted object' do
conference.program.event_types.first.update_attributes(title: 'New Event Title')
conference.program.event_types.first.destroy
get :revert_attribute, id: PaperTrail::Version.all[-2].id, attribute: 'title'
conference.reload
expect(flash[:alert]).to eq 'You are not authorized to access this page.'
end
it 'fails on trying to revert creation event' do
create(:lodging, conference: conference)
get :revert_attribute, id: PaperTrail::Version.last.id, attribute: 'name'
expect(flash[:alert]).to eq 'You are not authorized to access this page.'
end
it 'revert fails when attribute is invalid' do
conference.update_attributes(short_title: 'testtitle', description: 'Some random text')
before_conference_title = conference.title
# Note: even though title is a valid attribute of conference, it was not updated in the change we are trying to revert
get :revert_attribute, id: PaperTrail::Version.last.id, attribute: 'title'
conference.reload
expect(conference.short_title).to eq 'testtitle'
expect(conference.description).to eq 'Some random text'
expect(conference.title).to eq(before_conference_title)
expect(flash[:error]).to match('Revert failed. Attribute missing or invalid')
end
end
end
end

View file

@ -17,7 +17,6 @@ FactoryGirl.define do
factory :event_full do
difficulty_level
track
after(:build) do |event|
event.commercials << build(:event_commercial, commercialable: event)
event.difficulty_level = build(:difficulty_level, program: event.program)

View file

@ -3,5 +3,6 @@ FactoryGirl.define do
name { Faker::Commerce.department(2, true) }
description { Faker::Lorem.sentence }
color { Faker::Color.hex_color }
program
end
end

View file

@ -102,6 +102,9 @@ feature 'Has correct abilities' do
visit admin_conference_commercials_path(conference1.short_title)
expect(current_path).to eq(admin_conference_commercials_path(conference1.short_title))
visit admin_revision_history_path
expect(current_path).to eq(admin_revision_history_path)
end
scenario 'when user is cfp' do
@ -176,6 +179,9 @@ feature 'Has correct abilities' do
visit admin_conference_commercials_path(conference2.short_title)
expect(current_path).to eq(root_path)
visit admin_revision_history_path
expect(current_path).to eq(root_path)
end
scenario 'when user is info desk' do
@ -250,5 +256,8 @@ feature 'Has correct abilities' do
visit admin_conference_commercials_path(conference3.short_title)
expect(current_path).to eq(root_path)
visit admin_revision_history_path
expect(current_path).to eq(root_path)
end
end

View file

@ -22,6 +22,10 @@ ActiveRecord::Migration.maintain_test_schema!
require 'capybara/poltergeist'
require 'phantomjs'
# Adds rspec helper provided by paper_trail
# makes it easier to control when PaperTrail is enabled during testing.
require 'paper_trail/frameworks/rspec'
# Requires supporting ruby files with custom matchers and macros, etc, in
# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
# run as spec files by default. This means that files in spec/support that end