diff -Nur redmine-1.1.0/app/controllers/journals_controller.rb redmine-1.1.0-rollback/app/controllers/journals_controller.rb --- redmine-1.1.0/app/controllers/journals_controller.rb 2011-01-09 17:05:00.000000000 -0700 +++ redmine-1.1.0-rollback/app/controllers/journals_controller.rb 2011-02-11 17:15:13.829487200 -0700 @@ -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] + before_filter :find_journal, :only => [:edit, :rollback] before_filter :find_issue, :only => [:new] before_filter :find_optional_project, :only => [:index] - before_filter :authorize, :only => [:new, :edit] + before_filter :authorize, :only => [:new, :edit, :rollback] accept_key_auth :index helper :issues @@ -67,6 +67,8 @@ end def edit + (render_403; return false) unless @journal.editable_by?(User.current) + if request.post? @journal.update_attributes(:notes => params[:notes]) if params[:notes] @journal.destroy if @journal.details.empty? && @journal.notes.blank? @@ -77,11 +79,27 @@ end end end + + 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] = "" + 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.find(params[:id]) - (render_403; return false) unless @journal.editable_by?(User.current) @project = @journal.journalized.project rescue ActiveRecord::RecordNotFound render_404 diff -Nur redmine-1.1.0/app/models/issue.rb redmine-1.1.0-rollback/app/models/issue.rb --- redmine-1.1.0/app/models/issue.rb 2011-02-11 12:52:05.752402400 -0700 +++ redmine-1.1.0-rollback/app/models/issue.rb 2011-02-11 17:17:51.065874800 -0700 @@ -182,6 +182,11 @@ end issue end + + # parent_id is never set directly, see :after_save + def parent_id=(id) + @parent_issue = Issue.find_by_id(id) + end def status_id=(sid) self.status = nil @@ -358,6 +363,33 @@ @current_journal 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 != journals.last + + # 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 + journal.destroy if save(false) + end + # Return true if the issue is closed, otherwise false def closed? self.status.is_closed? @@ -785,6 +817,7 @@ # Callback on attachment deletion def attachment_removed(obj) + return if @rolling_back journal = init_journal(User.current) journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, diff -Nur redmine-1.1.0/app/models/journal.rb redmine-1.1.0-rollback/app/models/journal.rb --- redmine-1.1.0/app/models/journal.rb 2011-01-09 17:05:00.000000000 -0700 +++ redmine-1.1.0-rollback/app/models/journal.rb 2011-02-11 17:10:38.544713600 -0700 @@ -58,6 +58,20 @@ usr && usr.logged? && (usr.allowed_to?(:edit_issue_notes, project) || (self.user == usr && usr.allowed_to?(:edit_own_issue_notes, project))) end + def can_rollback?(usr = nil) + user = usr || User.current + editable_by?(user) && (details.empty? || user.allowed_to?(:rollback_issue_notes, project)) + 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 -Nur redmine-1.1.0/app/views/issues/_history.rhtml redmine-1.1.0-rollback/app/views/issues/_history.rhtml --- redmine-1.1.0/app/views/issues/_history.rhtml 2011-02-11 12:52:05.752402400 -0700 +++ redmine-1.1.0-rollback/app/views/issues/_history.rhtml 2011-02-11 17:10:38.560338800 -0700 @@ -2,6 +2,9 @@ <% for journal in journals %>

+ <% if journal == journals.last && 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(:label_rollback)) %> 
+ <% end %> <%= avatar(journal.user, :size => "24") %> <%= content_tag('a', '', :name => "note-#{journal.indice}")%> <%= authoring journal.created_on, journal.user, :label => :label_updated_time_by %>

diff -Nur redmine-1.1.0/config/locales/en.yml redmine-1.1.0-rollback/config/locales/en.yml --- redmine-1.1.0/config/locales/en.yml 2011-02-11 12:52:05.752402400 -0700 +++ redmine-1.1.0-rollback/config/locales/en.yml 2011-02-11 17:10:38.560338800 -0700 @@ -378,6 +378,7 @@ permission_add_issue_notes: Add notes permission_edit_issue_notes: Edit notes permission_edit_own_issue_notes: Edit own notes + permission_rollback_issue_notes: Rollback issue notes permission_move_issues: Move issues permission_delete_issues: Delete issues permission_manage_public_queries: Manage public queries @@ -793,6 +794,7 @@ label_project_copy_notifications: Send email notifications during the project copy label_principal_search: "Search for user or group:" label_user_search: "Search for user:" + label_rollback: Rollback button_login: Login button_submit: Submit diff -Nur redmine-1.1.0/lib/redmine.rb redmine-1.1.0-rollback/lib/redmine.rb --- redmine-1.1.0/lib/redmine.rb 2011-02-11 12:52:05.768027600 -0700 +++ redmine-1.1.0-rollback/lib/redmine.rb 2011-02-11 17:10:38.575964000 -0700 @@ -72,6 +72,7 @@ map.permission :add_issue_notes, {:issues => [:edit, :update], :journals => [:new]} 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 :move_issues, {:issue_moves => [:new, :create]}, :require => :loggedin map.permission :delete_issues, {:issues => :destroy}, :require => :member # Queries diff -Nur redmine-1.1.0/public/stylesheets/application.css redmine-1.1.0-rollback/public/stylesheets/application.css --- redmine-1.1.0/public/stylesheets/application.css 2011-02-11 12:52:05.768027600 -0700 +++ redmine-1.1.0-rollback/public/stylesheets/application.css 2011-02-11 15:59:42.537112400 -0700 @@ -938,6 +938,10 @@ float: right; } +.journal-rollback { + float: right; +} + h2 img { vertical-align:middle; } .hascontextmenu { cursor: context-menu; }