From a45e537bac0d9eba0516d867a8ce40dec890dc1b Mon Sep 17 00:00:00 2001 From: Nishanth Vijayan Date: Tue, 24 May 2016 23:54:46 +0530 Subject: [PATCH 1/3] 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 --- app/assets/javascripts/application.js | 1 + app/assets/javascripts/osem-datatables.js | 5 + .../javascripts/osem-revisionhistory.js | 10 ++ app/assets/stylesheets/osem.css.scss | 4 + app/controllers/admin/versions_controller.rb | 58 +++++++ app/helpers/application_helper.rb | 121 ++++++++++++++ app/models/ability.rb | 12 ++ app/models/campaign.rb | 2 + app/models/cfp.rb | 5 + app/models/comment.rb | 6 + app/models/commercial.rb | 10 ++ app/models/conference.rb | 2 +- app/models/contact.rb | 2 + app/models/difficulty_level.rb | 7 + app/models/email_settings.rb | 2 + app/models/event.rb | 10 +- app/models/event_type.rb | 6 + app/models/events_registration.rb | 8 + app/models/lodging.rb | 2 + app/models/program.rb | 36 ++-- app/models/registration.rb | 8 +- app/models/registration_period.rb | 2 + app/models/role.rb | 6 +- app/models/room.rb | 6 + app/models/splashpage.rb | 2 + app/models/sponsor.rb | 2 + app/models/sponsorship_level.rb | 2 + app/models/subscription.rb | 2 + app/models/target.rb | 2 + app/models/ticket.rb | 2 + app/models/track.rb | 6 + app/models/user.rb | 8 +- app/models/users_role.rb | 12 ++ app/models/venue.rb | 2 + app/models/vote.rb | 9 + .../admin/versions/_object_changes.html.haml | 28 ++++ .../versions/_object_desc_and_link.html.haml | 158 ++++++++++++++++++ app/views/admin/versions/index.html.haml | 77 +++++++++ .../layouts/_admin_sidebar_index.html.haml | 5 + app/views/layouts/_user_menu.html.haml | 5 + config/routes.rb | 4 + ...524050538_add_conference_id_to_versions.rb | 5 + .../20160614145614_add_id_to_users_roles.rb | 5 + db/schema.rb | 3 +- .../admin/versions_controller_spec.rb | 101 +++++++++++ spec/factories/events.rb | 1 - spec/factories/tracks.rb | 1 + spec/features/ability_spec.rb | 9 + spec/spec_helper.rb | 4 + 49 files changed, 760 insertions(+), 26 deletions(-) create mode 100644 app/assets/javascripts/osem-revisionhistory.js create mode 100644 app/controllers/admin/versions_controller.rb create mode 100644 app/models/users_role.rb create mode 100644 app/views/admin/versions/_object_changes.html.haml create mode 100644 app/views/admin/versions/_object_desc_and_link.html.haml create mode 100644 app/views/admin/versions/index.html.haml create mode 100644 db/migrate/20160524050538_add_conference_id_to_versions.rb create mode 100644 db/migrate/20160614145614_add_id_to_users_roles.rb create mode 100644 spec/controllers/admin/versions_controller_spec.rb diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index f6322490..8853d6e6 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -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 diff --git a/app/assets/javascripts/osem-datatables.js b/app/assets/javascripts/osem-datatables.js index c2b3b75e..f950159e 100644 --- a/app/assets/javascripts/osem-datatables.js +++ b/app/assets/javascripts/osem-datatables.js @@ -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' ]] + }); }); }); diff --git a/app/assets/javascripts/osem-revisionhistory.js b/app/assets/javascripts/osem-revisionhistory.js new file mode 100644 index 00000000..bd51435b --- /dev/null +++ b/app/assets/javascripts/osem-revisionhistory.js @@ -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(); + }); +}); diff --git a/app/assets/stylesheets/osem.css.scss b/app/assets/stylesheets/osem.css.scss index ccd96331..d2a2cd10 100644 --- a/app/assets/stylesheets/osem.css.scss +++ b/app/assets/stylesheets/osem.css.scss @@ -86,3 +86,7 @@ p.comment-body { #proposal-info div dt, #proposal-info div dd { display: inline-block; } + +.changeset{ + display: none; +} diff --git a/app/controllers/admin/versions_controller.rb b/app/controllers/admin/versions_controller.rb new file mode 100644 index 00000000..d103e874 --- /dev/null +++ b/app/controllers/admin/versions_controller.rb @@ -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 diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 8283150a..09f69484 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -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 diff --git a/app/models/ability.rb b/app/models/ability.rb index 2210a7cd..8891aabf 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -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) diff --git a/app/models/campaign.rb b/app/models/campaign.rb index 66257371..28cddbbd 100644 --- a/app/models/campaign.rb +++ b/app/models/campaign.rb @@ -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. # diff --git a/app/models/cfp.rb b/app/models/cfp.rb index 3cd15bf7..3e516ae6 100644 --- a/app/models/cfp.rb +++ b/app/models/cfp.rb @@ -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 diff --git a/app/models/comment.rb b/app/models/comment.rb index 1f501c3f..31531f8e 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -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 diff --git a/app/models/commercial.rb b/app/models/commercial.rb index 13540447..a2245d07 100644 --- a/app/models/commercial.rb +++ b/app/models/commercial.rb @@ -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 diff --git a/app/models/conference.rb b/app/models/conference.rb index 9079bd71..55cd8ed5 100644 --- a/app/models/conference.rb +++ b/app/models/conference.rb @@ -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 diff --git a/app/models/contact.rb b/app/models/contact.rb index 9f0b8c0f..3f8216df 100644 --- a/app/models/contact.rb +++ b/app/models/contact.rb @@ -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 diff --git a/app/models/difficulty_level.rb b/app/models/difficulty_level.rb index 9f066422..b5d971a7 100644 --- a/app/models/difficulty_level.rb +++ b/app/models/difficulty_level.rb @@ -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 diff --git a/app/models/email_settings.rb b/app/models/email_settings.rb index c9632932..5f7c8296 100644 --- a/app/models/email_settings.rb +++ b/app/models/email_settings.rb @@ -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, diff --git a/app/models/event.rb b/app/models/event.rb index dfc5f664..d2446847 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -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 diff --git a/app/models/event_type.rb b/app/models/event_type.rb index fd59aa0a..b00b36cd 100644 --- a/app/models/event_type.rb +++ b/app/models/event_type.rb @@ -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 diff --git a/app/models/events_registration.rb b/app/models/events_registration.rb index 187f4189..fcebd69f 100644 --- a/app/models/events_registration.rb +++ b/app/models/events_registration.rb @@ -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 diff --git a/app/models/lodging.rb b/app/models/lodging.rb index 48031eba..21e4a8b0 100644 --- a/app/models/lodging.rb +++ b/app/models/lodging.rb @@ -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 diff --git a/app/models/program.rb b/app/models/program.rb index 280471a2..49d84937 100644 --- a/app/models/program.rb +++ b/app/models/program.rb @@ -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 diff --git a/app/models/registration.rb b/app/models/registration.rb index d6a549fc..99e6766c 100644 --- a/app/models/registration.rb +++ b/app/models/registration.rb @@ -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 diff --git a/app/models/registration_period.rb b/app/models/registration_period.rb index b4e04a95..a7c251ac 100644 --- a/app/models/registration_period.rb +++ b/app/models/registration_period.rb @@ -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 diff --git a/app/models/role.rb b/app/models/role.rb index 9673e700..f194ad91 100644 --- a/app/models/role.rb +++ b/app/models/role.rb @@ -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 diff --git a/app/models/room.rb b/app/models/room.rb index 87dfb848..a789e2e5 100644 --- a/app/models/room.rb +++ b/app/models/room.rb @@ -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 diff --git a/app/models/splashpage.rb b/app/models/splashpage.rb index 1f7a5ff5..15447aa9 100644 --- a/app/models/splashpage.rb +++ b/app/models/splashpage.rb @@ -1,3 +1,5 @@ class Splashpage < ActiveRecord::Base belongs_to :conference + + has_paper_trail ignore: [:updated_at], meta: { conference_id: :conference_id } end diff --git a/app/models/sponsor.rb b/app/models/sponsor.rb index 7a843e63..985cef44 100644 --- a/app/models/sponsor.rb +++ b/app/models/sponsor.rb @@ -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 diff --git a/app/models/sponsorship_level.rb b/app/models/sponsorship_level.rb index f947a60b..d4630971 100644 --- a/app/models/sponsorship_level.rb +++ b/app/models/sponsorship_level.rb @@ -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 diff --git a/app/models/subscription.rb b/app/models/subscription.rb index 7f39adf3..1ee97ca6 100644 --- a/app/models/subscription.rb +++ b/app/models/subscription.rb @@ -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 diff --git a/app/models/target.rb b/app/models/target.rb index ea91b38a..2de7c757 100644 --- a/app/models/target.rb +++ b/app/models/target.rb @@ -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', diff --git a/app/models/ticket.rb b/app/models/ticket.rb index 54f5c3d7..8a620f79 100644 --- a/app/models/ticket.rb +++ b/app/models/ticket.rb @@ -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. diff --git a/app/models/track.rb b/app/models/track.rb index 11db2e8a..1d94f7d5 100644 --- a/app/models/track.rb +++ b/app/models/track.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index d3c847a2..1e86473d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 ## diff --git a/app/models/users_role.rb b/app/models/users_role.rb new file mode 100644 index 00000000..3a3afaba --- /dev/null +++ b/app/models/users_role.rb @@ -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 diff --git a/app/models/venue.rb b/app/models/venue.rb index aa65816c..302171b5 100644 --- a/app/models/venue.rb +++ b/app/models/venue.rb @@ -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 diff --git a/app/models/vote.rb b/app/models/vote.rb index e7da3c17..5b0e3fe7 100644 --- a/app/models/vote.rb +++ b/app/models/vote.rb @@ -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 diff --git a/app/views/admin/versions/_object_changes.html.haml b/app/views/admin/versions/_object_changes.html.haml new file mode 100644 index 00000000..b61d3c83 --- /dev/null +++ b/app/views/admin/versions/_object_changes.html.haml @@ -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 diff --git a/app/views/admin/versions/_object_desc_and_link.html.haml b/app/views/admin/versions/_object_desc_and_link.html.haml new file mode 100644 index 00000000..a34f4c4a --- /dev/null +++ b/app/views/admin/versions/_object_desc_and_link.html.haml @@ -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) diff --git a/app/views/admin/versions/index.html.haml b/app/views/admin/versions/index.html.haml new file mode 100644 index 00000000..af604820 --- /dev/null +++ b/app/views/admin/versions/index.html.haml @@ -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 diff --git a/app/views/layouts/_admin_sidebar_index.html.haml b/app/views/layouts/_admin_sidebar_index.html.haml index b7a5e9c6..b00bb546 100644 --- a/app/views/layouts/_admin_sidebar_index.html.haml +++ b/app/views/layouts/_admin_sidebar_index.html.haml @@ -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 diff --git a/app/views/layouts/_user_menu.html.haml b/app/views/layouts/_user_menu.html.haml index 26f02b5a..e626ddd9 100644 --- a/app/views/layouts/_user_menu.html.haml +++ b/app/views/layouts/_user_menu.html.haml @@ -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 diff --git a/config/routes.rb b/config/routes.rb index 25c253e5..fdcf5e99 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/db/migrate/20160524050538_add_conference_id_to_versions.rb b/db/migrate/20160524050538_add_conference_id_to_versions.rb new file mode 100644 index 00000000..345218f1 --- /dev/null +++ b/db/migrate/20160524050538_add_conference_id_to_versions.rb @@ -0,0 +1,5 @@ +class AddConferenceIdToVersions < ActiveRecord::Migration + def change + add_column :versions, :conference_id, :integer + end +end diff --git a/db/migrate/20160614145614_add_id_to_users_roles.rb b/db/migrate/20160614145614_add_id_to_users_roles.rb new file mode 100644 index 00000000..20153f96 --- /dev/null +++ b/db/migrate/20160614145614_add_id_to_users_roles.rb @@ -0,0 +1,5 @@ +class AddIdToUsersRoles < ActiveRecord::Migration + def change + add_column :users_roles, :id, :primary_key + end +end diff --git a/db/schema.rb b/db/schema.rb index 8494999a..f6c5bed2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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" diff --git a/spec/controllers/admin/versions_controller_spec.rb b/spec/controllers/admin/versions_controller_spec.rb new file mode 100644 index 00000000..4694837d --- /dev/null +++ b/spec/controllers/admin/versions_controller_spec.rb @@ -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 diff --git a/spec/factories/events.rb b/spec/factories/events.rb index 3ae38109..c306cdf9 100644 --- a/spec/factories/events.rb +++ b/spec/factories/events.rb @@ -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) diff --git a/spec/factories/tracks.rb b/spec/factories/tracks.rb index 3f35f539..1b7f8083 100644 --- a/spec/factories/tracks.rb +++ b/spec/factories/tracks.rb @@ -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 diff --git a/spec/features/ability_spec.rb b/spec/features/ability_spec.rb index 7107056f..8ac8a1ec 100644 --- a/spec/features/ability_spec.rb +++ b/spec/features/ability_spec.rb @@ -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 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 77e1ad17..3ee6db15 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -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 From 8a4a9552af5e13531aa9002b6184057dfc29e3e7 Mon Sep 17 00:00:00 2001 From: Nishanth Vijayan Date: Tue, 21 Jun 2016 12:22:12 +0530 Subject: [PATCH 2/3] Add rake task to set conference_id in pre-existing versions --- lib/tasks/version.rake | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 lib/tasks/version.rake diff --git a/lib/tasks/version.rake b/lib/tasks/version.rake new file mode 100644 index 00000000..084ff181 --- /dev/null +++ b/lib/tasks/version.rake @@ -0,0 +1,21 @@ +namespace :data do + desc 'Sets conference_id in all pre-existing PaperTrail::Version objects' + task set_conference_in_versions: :environment do + + PaperTrail::Version.where(conference_id: nil, item_type: ['Conference', 'Event']).each do |version| + # All pre-existing versions are either of Conference or Event + if version.item_type == 'Conference' + version.update_attributes(conference_id: version.item_id) + + elsif version.item_type == 'Event' + event = (version.item || version.reify || version.next.reify) + if event.try(:program) + version.update_attributes(conference_id: event.program.conference_id) + else + puts "Setting conference_id value failed for PaperTrail::Version object with ID=#{version.id}" + end + end + end + puts 'All done!' + end +end From fb03a87a8729f19ce2a82bdc96fbe180fda47681 Mon Sep 17 00:00:00 2001 From: Nishanth Vijayan Date: Wed, 22 Jun 2016 02:26:38 +0530 Subject: [PATCH 3/3] Feature test for Revision History --- spec/features/versions_spec.rb | 373 +++++++++++++++++++++++++++++++++ 1 file changed, 373 insertions(+) create mode 100644 spec/features/versions_spec.rb diff --git a/spec/features/versions_spec.rb b/spec/features/versions_spec.rb new file mode 100644 index 00000000..992061c7 --- /dev/null +++ b/spec/features/versions_spec.rb @@ -0,0 +1,373 @@ +require 'spec_helper' + +feature 'Version' do + let!(:conference) { create(:conference) } + let!(:organizer_role) { Role.find_by(name: 'organizer', resource: conference) } + let!(:organizer) { create(:user, role_ids: [organizer_role.id]) } + + before(:each) do + sign_in organizer + end + + scenario 'display changes in contact', feature: true, versioning: true, js: true do + visit edit_admin_conference_contact_path(conference.short_title) + fill_in 'contact_email', with: 'example@example.com' + fill_in 'contact_sponsor_email', with: 'sponsor@example.com' + fill_in 'contact_social_tag', with: 'example' + fill_in 'contact_googleplus', with: 'http:\\www.google.com' + click_button 'Update Contact' + + visit admin_revision_history_path + expect(page).to have_text("#{organizer.name} updated social tag, email, googleplus and sponsor email of contact details in conference #{conference.short_title}") + end + + scenario 'display changes in program', feature: true, versioning: true, js: true do + visit edit_admin_conference_program_path(conference.short_title) + fill_in 'program_rating', with: '4' + click_button 'Update Program' + + visit admin_revision_history_path + expect(page).to have_text("#{organizer.name} updated rating of program in conference #{conference.short_title}") + end + + scenario 'display changes in cfp', feature: true, versioning: true, js: true do + cfp = create(:cfp, program: conference.program) + cfp.update_attributes(start_date: (Date.today + 1).strftime('%d/%m/%Y'), end_date: (Date.today + 3).strftime('%d/%m/%Y')) + cfp.destroy + + visit admin_revision_history_path + expect(page).to have_text("Someone (probably via the console) created new cfp in conference #{conference.short_title}") + expect(page).to have_text("Someone (probably via the console) updated start date and end date of cfp in conference #{conference.short_title}") + expect(page).to have_text("Someone (probably via the console) deleted cfp in conference #{conference.short_title}") + end + + scenario 'display changes in registration_period', feature: true, versioning: true, js: true do + registration_period = create(:registration_period, conference: conference) + registration_period.update_attributes(start_date: (Date.today + 1).strftime('%d/%m/%Y'), end_date: (Date.today + 3).strftime('%d/%m/%Y')) + registration_period.destroy + + visit admin_revision_history_path + expect(page).to have_text("Someone (probably via the console) created new registration period in conference #{conference.short_title}") + expect(page).to have_text("Someone (probably via the console) updated start date and end date of registration period in conference #{conference.short_title}") + expect(page).to have_text("Someone (probably via the console) deleted registration period in conference #{conference.short_title}") + end + + scenario 'display changes in conference', feature: true, versioning: true, js: true do + new_conference = create(:conference, title: 'Test Conference') + organizer.add_role :organizer, new_conference + new_conference.update_attributes(title: 'New Con', short_title: 'NewCon') + + visit admin_revision_history_path + expect(page).to have_text('Someone (probably via the console) created new conference New Con') + expect(page).to have_text('Someone (probably via the console) created new event type Talk in conference NewCon') + expect(page).to have_text('Someone (probably via the console) created new event type Workshop in conference NewCon') + expect(page).to have_text('Someone (probably via the console) updated title and short title of conference New Con') + end + + scenario 'display changes in event_type', feature: true, versioning: true, js: true do + event_type = create(:event_type, program: conference.program, name: 'Discussion') + event_type.update_attributes(length: 90, maximum_abstract_length: 10000) + event_type.destroy + + visit admin_revision_history_path + expect(page).to have_text("Someone (probably via the console) created new event type Discussion in conference #{conference.short_title}") + expect(page).to have_text("Someone (probably via the console) updated length and maximum abstract length of event type Discussion in conference #{conference.short_title}") + expect(page).to have_text("Someone (probably via the console) deleted event type Discussion in conference #{conference.short_title}") + end + + scenario 'display changes in lodging', feature: true, versioning: true, js: true do + lodging = create(:lodging, conference: conference, name: 'Hotel XYZ') + lodging.update_attributes(description: 'Nice view,close to venue', website_link: 'http://www.example.com') + lodging.destroy + + visit admin_revision_history_path + expect(page).to have_text("Someone (probably via the console) created new lodging Hotel XYZ in conference #{conference.short_title}") + expect(page).to have_text("Someone (probably via the console) updated description and website link of lodging Hotel XYZ in conference #{conference.short_title}") + expect(page).to have_text("Someone (probably via the console) deleted lodging Hotel XYZ in conference #{conference.short_title}") + end + + scenario 'display changes in role', feature: true, versioning: true, js: true do + visit edit_admin_conference_role_path(conference.short_title, 'cfp') + fill_in 'role_description', with: 'For the members of the call for papers team' + click_button 'Update Role' + + visit admin_revision_history_path(conference_id: conference.short_title) + expect(page).to have_text("#{organizer.name} updated description of role cfp in conference #{conference.short_title}") + end + + scenario 'display changes in room', feature: true, versioning: true, js: true do + venue = create(:venue, conference: conference) + room = create(:room, venue: venue, name: 'Auditorium') + room.update_attributes(size: 120) + room.destroy + + visit admin_revision_history_path + expect(page).to have_text("Someone (probably via the console) created new room Auditorium in conference #{conference.short_title}") + expect(page).to have_text("Someone (probably via the console) updated size of room Auditorium in conference #{conference.short_title}") + expect(page).to have_text("Someone (probably via the console) deleted room Auditorium in conference #{conference.short_title}") + end + + scenario 'display changes in sponsor', feature: true, versioning: true, js: true do + conference.sponsorship_levels << create_list(:sponsorship_level, 2, conference: conference) + sponsor = create(:sponsor, conference: conference, name: 'SUSE', sponsorship_level: conference.sponsorship_levels.first) + sponsor.update_attributes(website_url: 'https://www.suse.com/company/history', sponsorship_level: conference.sponsorship_levels.second) + sponsor.destroy + + visit admin_revision_history_path + expect(page).to have_text("Someone (probably via the console) created new sponsor SUSE in conference #{conference.short_title}") + expect(page).to have_text("Someone (probably via the console) updated website url and sponsorship level of sponsor SUSE in conference #{conference.short_title}") + expect(page).to have_text("Someone (probably via the console) deleted sponsor SUSE in conference #{conference.short_title}") + end + + scenario 'display changes in sponsorship_level', feature: true, versioning: true, js: true do + sponsorship_level = create(:sponsorship_level, conference: conference) + sponsorship_level.update_attributes(title: 'Gold') + sponsorship_level.destroy + + visit admin_revision_history_path + expect(page).to have_text("Someone (probably via the console) created new sponsorship level Gold in conference #{conference.short_title}") + expect(page).to have_text("Someone (probably via the console) updated title of sponsorship level Gold in conference #{conference.short_title}") + expect(page).to have_text("Someone (probably via the console) deleted sponsorship level Gold in conference #{conference.short_title}") + end + + scenario 'display changes in ticket', feature: true, versioning: true, js: true do + ticket = create(:ticket, conference: conference, title: 'Gold') + ticket.update_attributes(price: 50, description: 'Premium Ticket') + ticket.destroy + + visit admin_revision_history_path + expect(page).to have_text("Someone (probably via the console) created new ticket Gold in conference #{conference.short_title}") + expect(page).to have_text("Someone (probably via the console) updated price cents and description of ticket Gold in conference #{conference.short_title}") + expect(page).to have_text("Someone (probably via the console) deleted ticket Gold in conference #{conference.short_title}") + end + + scenario 'display changes in track', feature: true, versioning: true, js: true do + track = create(:track, program: conference.program, name: 'Distribution') + track.update_attributes(description: 'Events about Linux distributions') + track.destroy + + visit admin_revision_history_path + expect(page).to have_text("Someone (probably via the console) created new track Distribution in conference #{conference.short_title}") + expect(page).to have_text("Someone (probably via the console) updated description of track Distribution in conference #{conference.short_title}") + expect(page).to have_text("Someone (probably via the console) deleted track Distribution in conference #{conference.short_title}") + end + + scenario 'display changes in venue', feature: true, versioning: true, js: true do + venue = create(:venue, conference: conference, name: 'Example University') + venue.update_attributes(website: 'www.example.com new', description: 'Just another beautiful venue') + venue.destroy + + visit admin_revision_history_path + expect(page).to have_text("Someone (probably via the console) created new venue Example University in conference #{conference.short_title}") + expect(page).to have_text("Someone (probably via the console) updated website and description of venue Example University in conference #{conference.short_title}") + expect(page).to have_text("Someone (probably via the console) deleted venue Example University in conference #{conference.short_title}") + end + + scenario 'display changes in event', feature: true, versioning: true, js: true do + visit new_conference_program_proposal_path(conference_id: conference.short_title) + fill_in 'event_title', with: 'ABC' + fill_in 'event_abstract', with: 'Lorem ipsum abstract' + select('Talk - 30 min', from: 'event[event_type_id]') + click_button 'Create Proposal' + + click_link 'Edit' + fill_in 'event_subtitle', with: 'My event subtitle' + select('Easy', from: 'event[difficulty_level_id]') + click_button 'Update Proposal' + + visit admin_conference_program_events_path(conference.short_title) + click_button 'New' + click_link 'Reject event' + + visit conference_program_proposal_index_path(conference_id: conference.short_title) + click_link 'Re-Submit' + + visit admin_conference_program_events_path(conference.short_title) + click_button 'New' + click_link 'Accept event' + + visit conference_program_proposal_index_path(conference_id: conference.short_title) + click_link 'Confirm' + + visit admin_conference_program_events_path(conference.short_title) + click_button 'Confirmed' + click_link 'Cancel event' + + visit admin_revision_history_path + expect(page).to have_text("#{organizer.name} submitted new event ABC in conference #{conference.short_title}") + expect(page).to have_text("#{organizer.name} updated difficulty level and subtitle of event ABC in conference #{conference.short_title}") + expect(page).to have_text("#{organizer.name} rejected event ABC in conference #{conference.short_title}") + expect(page).to have_text("#{organizer.name} resubmitted event ABC in conference #{conference.short_title}") + expect(page).to have_text("#{organizer.name} accepted event ABC in conference #{conference.short_title}") + expect(page).to have_text("#{organizer.name} confirmed event ABC in conference #{conference.short_title}") + expect(page).to have_text("#{organizer.name} canceled event ABC in conference #{conference.short_title}") + end + + scenario 'display changes in difficulty levels', feature: true, versioning: true, js: true do + difficulty_level = create(:difficulty_level, program: conference.program, title: 'Expert') + difficulty_level.update_attributes(description: 'Only for Experts') + difficulty_level.destroy + + visit admin_revision_history_path + expect(page).to have_text("Someone (probably via the console) created new difficulty level Expert in conference #{conference.short_title}") + expect(page).to have_text("Someone (probably via the console) updated description of difficulty level Expert in conference #{conference.short_title}") + expect(page).to have_text("Someone (probably via the console) deleted difficulty level Expert in conference #{conference.short_title}") + end + + scenario 'display changes in splashpages', feature: true, versioning: true, js: true do + visit admin_conference_splashpage_path(conference.short_title) + click_link 'Create Splashpage' + click_button 'Save Splashpage' + + click_link 'Edit' + check('Make splash page public') + check('Display tracks on the splashpage?') + check('Display the registration period on the splashpage?') + click_button 'Save Splashpage' + + click_link 'Delete' + visit admin_revision_history_path + expect(page).to have_text("#{organizer.name} created new splashpage in conference #{conference.short_title}") + expect(page).to have_text("#{organizer.name} updated public, include tracks and include registrations of splashpage in conference #{conference.short_title}") + expect(page).to have_text("#{organizer.name} deleted splashpage in conference #{conference.short_title}") + end + + scenario 'displays users subscribe/unsubscribe to conferences', feature: true, versioning: true, js: true do + visit root_path + click_link 'Subscribe' + click_link 'Unsubscribe' + PaperTrail::Version.last.reify.save! + PaperTrail::Version.last.item.destroy! + + visit admin_revision_history_path + expect(page).to have_text("#{organizer.name} subscribed to conference #{conference.title}") + expect(page).to have_text("#{organizer.name} unsubscribed from conference #{conference.title}") + expect(page).to have_text("Someone (probably via the console) subscribed #{organizer.name} to conference #{conference.title}") + expect(page).to have_text("Someone (probably via the console) unsubscribed #{organizer.name} from conference #{conference.title}") + end + + scenario 'display changes in conference commercials', feature: true, versioning: true, js: true do + conference_commercial = create(:conference_commercial, commercialable: conference) + conference_commercial.update_attributes(url: 'https://www.youtube.com/watch?v=VNkDJk5_9eU') + conference_commercial.destroy + + visit admin_revision_history_path + expect(page).to have_text("Someone (probably via the console) created new commercial in conference #{conference.short_title}") + expect(page).to have_text("Someone (probably via the console) updated url of commercial in conference #{conference.short_title}") + expect(page).to have_text("Someone (probably via the console) deleted commercial in conference #{conference.short_title}") + end + + scenario 'display changes in event commercials', feature: true, versioning: true, js: true do + event = create(:event, program: conference.program) + event_commercial = create(:event_commercial, commercialable: event, url: 'https://www.youtube.com/watch?v=M9bq_alk-sw') + event_commercial.update_attributes(url: 'https://www.youtube.com/watch?v=VNkDJk5_9eU') + event_commercial.destroy + + visit admin_revision_history_path + expect(page).to have_text("Someone (probably via the console) created new commercial in event #{event.title} in conference #{conference.short_title}") + expect(page).to have_text("Someone (probably via the console) updated url of commercial in event #{event.title} in conference #{conference.short_title}") + expect(page).to have_text("Someone (probably via the console) deleted commercial in event #{event.title} in conference #{conference.short_title}") + end + + scenario 'display changes in users_role', feature: true, versioning: true, js: true do + user = create(:user) + user.add_role :cfp, conference + user.remove_role :cfp, conference + + visit admin_revision_history_path + expect(page).to have_text("added role cfp to user #{user.name} in conference #{conference.short_title}") + expect(page).to have_text("removed role cfp from user #{user.name} in conference #{conference.short_title}") + end + + scenario 'display changes in email settings', feature: true, versioning: true, js: true do + conference.email_settings.update_attributes(registration_subject: 'xxxxx', registration_body: 'yyyyy', accepted_subject: 'zzzzz') + + visit admin_revision_history_path + expect(page).to have_text("Someone (probably via the console) updated registration subject, registration body and accepted subject + of email settings in conference #{conference.short_title}") + end + + scenario 'display changes in conference registrations', feature: true, versioning: true, js: true do + Registration.create(user: organizer, conference: conference) + Registration.last.destroy + + visit admin_revision_history_path + expect(page).to have_text("Someone (probably via the console) registered #{organizer.name} to conference #{conference.title}") + expect(page).to have_text("Someone (probably via the console) unregistered #{organizer.name} from conference #{conference.title}") + end + + scenario 'display changes in event registration', feature: true, versioning: true, js: true do + registration = Registration.create(user: organizer, conference: conference) + event = create(:event, program: conference.program) + EventsRegistration.create(registration: registration, event: event) + EventsRegistration.first.update_attributes(attended: true) + EventsRegistration.last.destroy + # Here registration is deleted to ensure the event registration related change still displays the asociated user's name + registration.destroy + + visit admin_revision_history_path + expect(page).to have_text("Someone (probably via the console) registered #{organizer.name} to event #{event.title} in conference #{conference.short_title}") + expect(page).to have_text("Someone (probably via the console) updated attended of #{organizer.name}'s registration for event #{event.title} in conference #{conference.short_title}") + expect(page).to have_text("Someone (probably via the console) unregistered #{organizer.name} from event #{event.title} in conference #{conference.short_title}") + end + + scenario 'display changes in target', feature: true, versioning: true, js: true do + target = create(:target, conference: conference) + target.update_attributes(due_date: Date.today, target_count: 1000) + target.destroy + + visit admin_revision_history_path + expect(page).to have_text("Someone (probably via the console) created new target 1000 Submissions by #{Date.today} in conference #{conference.short_title}") + expect(page).to have_text("Someone (probably via the console) updated due date and target count of target 1000 Submissions by #{Date.today} in conference #{conference.short_title}") + expect(page).to have_text("Someone (probably via the console) deleted target 1000 Submissions by #{Date.today} in conference #{conference.short_title}") + end + + scenario 'display changes in comment', feature: true, versioning: true, js: true do + event = create(:event, program: conference.program) + visit admin_conference_program_event_path(conference_id: conference.short_title, id: event.id) + click_link 'Comments (0)' + fill_in 'comment_body', with: 'Sample comment' + click_button 'Add Comment' + Comment.last.destroy + PaperTrail::Version.last.reify.save + + visit admin_revision_history_path + expect(page).to have_text("#{organizer.name} commented on event #{event.title} in conference #{conference.short_title}") + expect(page).to have_text("Someone (probably via the console) deleted #{organizer.name}'s comment on event #{event.title} in conference #{conference.short_title}") + expect(page).to have_text("Someone (probably via the console) re-added #{organizer.name}'s comment on event #{event.title} in conference #{conference.short_title}") + end + + scenario 'display changes in campaign', feature: true, versioning: true, js: true do + campaign = create(:campaign, conference: conference, name: 'Test Campaign', utm_campaign: 'campaign') + campaign.update_attributes(utm_source: 'source', utm_medium: 'medium', utm_term: 'term', utm_content: 'content') + campaign.destroy + + visit admin_revision_history_path + expect(page).to have_text("Someone (probably via the console) created new campaign Test Campaign in conference #{conference.short_title}") + expect(page).to have_text("Someone (probably via the console) updated utm source, utm medium, utm term and utm content of campaign Test Campaign in conference #{conference.short_title}") + expect(page).to have_text("Someone (probably via the console) deleted campaign Test Campaign in conference #{conference.short_title}") + end + + scenario 'display password reset requests', feature: true, versioning: true, js: true do + user = create(:user) + user.send_reset_password_instructions + + visit admin_revision_history_path + expect(page).to have_text("Someone requested password reset of user #{user.name}") + end + + scenario 'display user signups', feature: true, versioning: true, js: true do + create(:user, name: 'testname') + + visit admin_revision_history_path + expect(page).to have_text('testname signed up') + end + + scenario 'display updates to user', feature: true, versioning: true, js: true do + user = create(:user) + user.update_attributes(nickname: 'testnick', affiliation: 'openSUSE') + + visit admin_revision_history_path + expect(page).to have_text("Someone (probably via the console) updated nickname and affiliation of user #{user.name}") + end +end