diff -ruN redmine-2.4.4/app/controllers/journals_controller.rb redmine-2.4.4-rollback/app/controllers/journals_controller.rb
--- redmine-2.4.4/app/controllers/journals_controller.rb 2014-03-02 04:28:20.000000000 -0700
+++ redmine-2.4.4-rollback/app/controllers/journals_controller.rb 2014-04-17 10:33:21.234634640 -0600
@@ -16,10 +16,10 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class JournalsController < ApplicationController
- before_filter :find_journal, :only => [:edit, :diff]
+ before_filter :find_journal, :only => [:edit, :diff, :rollback]
before_filter :find_issue, :only => [:new]
before_filter :find_optional_project, :only => [:index]
- before_filter :authorize, :only => [:new, :edit, :diff]
+ before_filter :authorize, :only => [:new, :edit, :diff, :rollback]
accept_rss_auth :index
menu_item :issues
@@ -94,7 +94,24 @@
end
end
- private
+ def rollback
+ (render_403; return false) unless @journal.can_rollback?(User.current)
+
+ if @journal.rollback
+ flash[:notice] = l(:notice_successful_update)
+ else
+ # can't seem to bring in the helper method 'error_messages_for'
+ # and injecting it into show.rhtml doesn't seem to work, since
+ # the @issue loses the errors on redirect (due to issue reload)
+ flash[:error] = "
" + @journal.errors.full_messages.map {|msg| "- " + ERB::Util.html_escape(msg) + "
"}.join + "
"
+ end
+ respond_to do |format|
+ format.html { redirect_to :controller => 'issues', :action => 'show', :id => @journal.journalized_id }
+ format.api { render_validation_errors(@issue) }
+ end
+ end
+
+private
def find_journal
@journal = Journal.visible.find(params[:id])
diff -ruN redmine-2.4.4/app/models/issue.rb redmine-2.4.4-rollback/app/models/issue.rb
--- redmine-2.4.4/app/models/issue.rb 2014-03-02 04:28:19.000000000 -0700
+++ redmine-2.4.4-rollback/app/models/issue.rb 2014-04-17 10:33:21.234634640 -0600
@@ -672,6 +672,36 @@
end
end
+ # rollback the changes in a journal, the journal is destroyed on success
+ def rollback(journal)
+ # only allow rollback of journal details for the last journal
+ (errors.add_to_base(l(:notice_locking_conflict)); return) if !journal.last_valid_journal?(journals)
+
+ # avoid the creation of journals during rollback (see 'attachment removed')
+ @rolling_back = true
+
+ # roll back each change detailed by the journal
+ journal.details.each do |d|
+ case d.property
+ # issue attribute change
+ when 'attr'; send "#{d.prop_key}=", d.old_value
+ # rollback custom field change
+ when 'cf'; custom_field_values.each {|v| v.value = d.old_value if v.custom_field_id == d.prop_key.to_i}
+ # remove any added attachments (we can't recover removed attachments)
+ when 'attachment'; attachments.each {|v| attachments.delete(v) if v.id == d.prop_key.to_i}
+ end
+ end
+
+ # allow the creation of journals again
+ remove_instance_variable(:@rolling_back)
+
+ # destroy the journal once we save the issue changes
+ if save(:validate => false)
+ journal.rolled_back = true
+ journal.save
+ end
+ end
+
# Return true if the issue is closed, otherwise false
def closed?
self.status.is_closed?
diff -ruN redmine-2.4.4/app/models/journal.rb redmine-2.4.4-rollback/app/models/journal.rb
--- redmine-2.4.4/app/models/journal.rb 2014-03-02 04:28:19.000000000 -0700
+++ redmine-2.4.4-rollback/app/models/journal.rb 2014-04-17 10:33:21.234634640 -0600
@@ -95,6 +95,27 @@
usr && usr.logged? && (usr.allowed_to?(:edit_issue_notes, project) || (self.user == usr && usr.allowed_to?(:edit_own_issue_notes, project)))
end
+ def last_valid_journal?(journals)
+ self == journals.last
+ end
+
+ def can_rollback?(usr = nil)
+ user ||= User.current
+ editable_by?(user) && (details.empty? || user.allowed_to?(:rollback_issue_notes, project))
+ end
+
+ def show?
+ !self.rolled_back || User.current.pref.hide_rolled_back_issue_notes == '0'
+ end
+
+ # rollback the changes in a journal, the journal is destroyed on success
+ def rollback
+ # we could have details to rollback, so let the issue take care of this more complicated task
+ journalized.rollback(self) ||
+ # on failure, collect the error messages from the issue on failure
+ (journalized.errors.each_full {|msg| errors.add_to_base(msg)}; false)
+ end
+
def project
journalized.respond_to?(:project) ? journalized.project : nil
end
diff -ruN redmine-2.4.4/app/models/user_preference.rb redmine-2.4.4-rollback/app/models/user_preference.rb
--- redmine-2.4.4/app/models/user_preference.rb 2014-03-02 04:28:19.000000000 -0700
+++ redmine-2.4.4-rollback/app/models/user_preference.rb 2014-04-17 10:35:35.682637045 -0600
@@ -57,6 +57,9 @@
def warn_on_leaving_unsaved; self[:warn_on_leaving_unsaved] || '1'; end
def warn_on_leaving_unsaved=(value); self[:warn_on_leaving_unsaved]=value; end
+ def hide_rolled_back_issue_notes; self[:hide_rolled_back_issue_notes] || '0'; end
+ def hide_rolled_back_issue_notes=(value); self[:hide_rolled_back_issue_notes]=value; end
+
def no_self_notified; (self[:no_self_notified] == true || self[:no_self_notified] == '1'); end
def no_self_notified=(value); self[:no_self_notified]=value; end
end
diff -ruN redmine-2.4.4/app/views/issues/_history.html.erb redmine-2.4.4-rollback/app/views/issues/_history.html.erb
--- redmine-2.4.4/app/views/issues/_history.html.erb 2014-03-02 04:28:20.000000000 -0700
+++ redmine-2.4.4-rollback/app/views/issues/_history.html.erb 2014-04-17 12:04:34.686732511 -0600
@@ -1,8 +1,17 @@
<% reply_links = authorize_for('issues', 'edit') -%>
<% for journal in journals %>
+ <% if journal.show? %>
+
+ <% if journal.rolled_back? %>
+
+ <% end %>
+
<%= link_to "##{journal.indice}", {:anchor => "note-#{journal.indice}"}, :class => "journal-link" %>
+ <% if journal.last_valid_journal?(journals) && journal.can_rollback? %>
+
<%= link_to(image_tag('cancel.png'), {:controller => 'journals', :action => 'rollback', :id => journal}, :confirm => l(:text_are_you_sure), :method => :post, :title => l(:button_rollback)) %>
+ <% end %>
<%= avatar(journal.user, :size => "24") %>
<%= authoring journal.created_on, journal.user, :label => :label_updated_time_by %>
@@ -17,6 +26,12 @@
<%= call_hook(:view_issues_history_journal_bottom, { :journal => journal }) %>
+
+ <% if journal.rolled_back? %>
+
+ <% end %>
+
+<% end %>
<% end %>
<% heads_for_wiki_formatter if User.current.allowed_to?(:edit_issue_notes, issue.project) || User.current.allowed_to?(:edit_own_issue_notes, issue.project) %>
diff -ruN redmine-2.4.4/app/views/users/_preferences.html.erb redmine-2.4.4-rollback/app/views/users/_preferences.html.erb
--- redmine-2.4.4/app/views/users/_preferences.html.erb 2014-03-02 04:28:20.000000000 -0700
+++ redmine-2.4.4-rollback/app/views/users/_preferences.html.erb 2014-04-17 10:33:21.234634640 -0600
@@ -3,4 +3,5 @@
<%= pref_fields.time_zone_select :time_zone, nil, :include_blank => true %>
<%= pref_fields.select :comments_sorting, [[l(:label_chronological_order), 'asc'], [l(:label_reverse_chronological_order), 'desc']] %>
<%= pref_fields.check_box :warn_on_leaving_unsaved %>
+<%= pref_fields.check_box :hide_rolled_back_issue_notes %>
<% end %>
diff -ruN redmine-2.4.4/config/locales/en.yml redmine-2.4.4-rollback/config/locales/en.yml
--- redmine-2.4.4/config/locales/en.yml 2014-03-02 04:28:21.000000000 -0700
+++ redmine-2.4.4-rollback/config/locales/en.yml 2014-04-17 11:02:25.618665831 -0600
@@ -337,6 +337,7 @@
field_inherit_members: Inherit members
field_generate_password: Generate password
field_must_change_passwd: Must change password at next logon
+ field_hide_rolled_back_issue_notes: "Hide rolled-back issue notes"
setting_app_title: Application title
setting_app_subtitle: Application subtitle
@@ -472,6 +473,7 @@
permission_export_wiki_pages: Export wiki pages
permission_manage_subtasks: Manage subtasks
permission_manage_related_issues: Manage related issues
+ permission_rollback_issue_notes: Rollback issue notes
project_module_issue_tracking: Issue tracking
project_module_time_tracking: Time tracking
@@ -956,6 +958,7 @@
button_delete_my_account: Delete my account
button_close: Close
button_reopen: Reopen
+ button_rollback: Rollback
status_active: active
status_registered: registered
diff -ruN redmine-2.4.4/config/routes.rb redmine-2.4.4-rollback/config/routes.rb
--- redmine-2.4.4/config/routes.rb 2014-04-17 12:06:26.590734511 -0600
+++ redmine-2.4.4-rollback/config/routes.rb 2014-04-17 12:07:01.410735133 -0600
@@ -51,6 +51,7 @@
match '/journals/diff/:id', :to => 'journals#diff', :id => /\d+/, :via => :get
match '/journals/edit/:id', :to => 'journals#edit', :id => /\d+/, :via => [:get, :post]
+ match '/journals/rollback/:id', :to => 'journals#rollback', :id => /\d+/, :via => :post
get '/projects/:project_id/issues/gantt', :to => 'gantts#show', :as => 'project_gantt'
get '/issues/gantt', :to => 'gantts#show'
diff -ruN redmine-2.4.4/db/migrate/20140417000000_add_journals_rolled_back.rb redmine-2.4.4-rollback/db/migrate/20140417000000_add_journals_rolled_back.rb
--- redmine-2.4.4/db/migrate/20140417000000_add_journals_rolled_back.rb 1969-12-31 17:00:00.000000000 -0700
+++ redmine-2.4.4-rollback/db/migrate/20140417000000_add_journals_rolled_back.rb 2014-04-17 10:33:21.238634641 -0600
@@ -0,0 +1,9 @@
+class AddJournalsRolledBack < ActiveRecord::Migration
+ def up
+ add_column :journals, :rolled_back, :boolean, :default => 0
+ end
+
+ def down
+ remove_column :journals, :rolled_back
+ end
+end
diff -ruN redmine-2.4.4/lib/redmine.rb redmine-2.4.4-rollback/lib/redmine.rb
--- redmine-2.4.4/lib/redmine.rb 2014-03-02 04:28:23.000000000 -0700
+++ redmine-2.4.4-rollback/lib/redmine.rb 2014-04-17 11:07:10.274670924 -0600
@@ -118,6 +118,7 @@
map.permission :add_issue_notes, {:issues => [:edit, :update, :update_form], :journals => [:new], :attachments => :upload}
map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
+ map.permission :rollback_issue_notes, {:journals => [:rollback]}
map.permission :view_private_notes, {}, :read => true, :require => :member
map.permission :set_notes_private, {}, :require => :member
map.permission :move_issues, {:issues => [:bulk_edit, :bulk_update]}, :require => :loggedin