From aef2a43890c2306e681a9b791be8d01140ac627e Mon Sep 17 00:00:00 2001 From: Frederico Camara Date: Thu, 31 Oct 2019 17:03:59 -0300 Subject: [PATCH 3/3] Rollback functionality --- app/controllers/journals_controller.rb | 20 ++++++- app/helpers/journals_helper.rb | 12 +++- app/models/issue.rb | 30 ++++++++++ app/models/journal.rb | 21 +++++++ app/models/user_preference.rb | 4 ++ app/views/issues/_history.html.erb | 57 +++++++++++-------- app/views/users/_preferences.html.erb | 1 + config/locales/en.yml | 3 + config/locales/pt-BR.yml | 3 + config/routes.rb | 1 + ...20140417000000_add_journals_rolled_back.rb | 9 +++ lib/redmine.rb | 5 +- 12 files changed, 136 insertions(+), 30 deletions(-) create mode 100644 db/migrate/20140417000000_add_journals_rolled_back.rb diff --git a/app/controllers/journals_controller.rb b/app/controllers/journals_controller.rb index 7f07b38a8..9ccce01f2 100644 --- a/app/controllers/journals_controller.rb +++ b/app/controllers/journals_controller.rb @@ -16,10 +16,10 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class JournalsController < ApplicationController - before_action :find_journal, :only => [:edit, :update, :diff] + before_action :find_journal, :only => [:edit, :update, :diff, :rollback] before_action :find_issue, :only => [:new] before_action :find_optional_project, :only => [:index] - before_action :authorize, :only => [:new, :edit, :update, :diff] + before_action :authorize, :only => [:new, :edit, :update, :diff, :rollback] accept_rss_auth :index menu_item :issues @@ -96,6 +96,22 @@ class JournalsController < ApplicationController 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, :anchor => "note-#{@journal.issue.journals.index(@journal) + 1}" } + format.api { render_validation_errors(@issue) } + end + end + private def find_journal diff --git a/app/helpers/journals_helper.rb b/app/helpers/journals_helper.rb index 5e817e862..9780e462d 100644 --- a/app/helpers/journals_helper.rb +++ b/app/helpers/journals_helper.rb @@ -28,7 +28,15 @@ module JournalsHelper # Returns the action links for an issue journal def render_journal_actions(issue, journal, options={}) links = [] - if journal.notes.present? + if journal.last_valid_journal?(issue.journals) & journal.can_rollback? + links << link_to(l(:button_cancel), + rollback_journal_path(journal), + :method => 'post', :data => {:confirm => l(:text_are_you_sure)}, + :title => l(:button_rollback), + :class => 'icon-only icon-cancel' + ) + end + if journal.notes.present? & !journal.rolled_back? if options[:reply_links] links << link_to(l(:button_quote), quoted_issue_path(issue, :journal_id => journal), @@ -49,7 +57,7 @@ module JournalsHelper links << link_to(l(:button_delete), journal_path(journal, :journal => {:notes => ""}), :remote => true, - :method => 'put', :data => {:confirm => l(:text_are_you_sure)}, + :method => 'put', :data => {:confirm => l(:text_are_you_sure)}, :title => l(:button_delete), :class => 'icon-only icon-del' ) diff --git a/app/models/issue.rb b/app/models/issue.rb index efd2789ed..7481d2508 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -883,6 +883,36 @@ class Issue < ActiveRecord::Base 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 :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? status.present? && status.is_closed? diff --git a/app/models/journal.rb b/app/models/journal.rb index ce3f9d0b3..7630e8654 100644 --- a/app/models/journal.rb +++ b/app/models/journal.rb @@ -114,6 +114,27 @@ class Journal < ActiveRecord::Base 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.reject{|j| j.rolled_back}.sort_by(&:id).last + end + + def can_rollback?(user = 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.full_messages.each {|msg| errors.add :base, msg}; false) + end + def project journalized.respond_to?(:project) ? journalized.project : nil end diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb index 7373290c5..27450dcec 100644 --- a/app/models/user_preference.rb +++ b/app/models/user_preference.rb @@ -29,6 +29,7 @@ class UserPreference < ActiveRecord::Base 'time_zone', 'comments_sorting', 'warn_on_leaving_unsaved', + 'hide_rolled_back_issue_notes', 'no_self_notified', 'textarea_font' @@ -79,6 +80,9 @@ class UserPreference < ActiveRecord::Base 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 diff --git a/app/views/issues/_history.html.erb b/app/views/issues/_history.html.erb index aa8ecbf80..c5a6dc754 100644 --- a/app/views/issues/_history.html.erb +++ b/app/views/issues/_history.html.erb @@ -2,34 +2,43 @@ <% for journal in journals %>
-
- <%= render_journal_actions(issue, journal, :reply_links => reply_links) %> - #<%= journal.indice %> -
-

- <%= avatar(journal.user, :size => "24") %> - <%= authoring journal.created_on, journal.user, :label => :label_updated_time_by %> - <%= render_private_notes_indicator(journal) %> -

+ <% if journal.show? %> + <% if journal.rolled_back? %> + + <% end %> +
+ <%= render_journal_actions(issue, journal, :reply_links => reply_links) %> + #<%= journal.indice %> +
+

+ <%= avatar(journal.user, :size => "24") %> + <%= authoring journal.created_on, journal.user, :label => :label_updated_time_by %> + <%= render_private_notes_indicator(journal) %> +

- <% if journal.details.any? %> -
    - <% details_to_strings(journal.visible_details).each do |string| %> -
  • <%= string %>
  • - <% end %> -
- <% if Setting.thumbnails_enabled? && (thumbnail_attachments = journal_thumbnail_attachments(journal)).any? %> -
- <% thumbnail_attachments.each do |attachment| %> -
<%= thumbnail_tag(attachment) %>
+ <% if journal.details.any? %> +
    + <% details_to_strings(journal.visible_details).each do |string| %> +
  • <%= string %>
  • + <% end %> +
+ <% if Setting.thumbnails_enabled? && (thumbnail_attachments = journal_thumbnail_attachments(journal)).any? %> +
+ <% thumbnail_attachments.each do |attachment| %> +
<%= thumbnail_tag(attachment) %>
+ <% end %> +
+ <% end %> <% end %> -
- <% end %> - <% end %> - <%= render_notes(issue, journal, :reply_links => reply_links) unless journal.notes.blank? %> + <%= render_notes(issue, journal, :reply_links => reply_links) unless journal.notes.blank? %> + <% if journal.rolled_back? %> +
+ <% end %> + <% end %>
- <%= call_hook(:view_issues_history_journal_bottom, { :journal => journal }) %> +<%= call_hook(:view_issues_history_journal_bottom, { :journal => journal }) %> + <% 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 --git a/app/views/users/_preferences.html.erb b/app/views/users/_preferences.html.erb index f8769125e..48737b5f7 100644 --- a/app/views/users/_preferences.html.erb +++ b/app/views/users/_preferences.html.erb @@ -3,5 +3,6 @@

<%= 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 %>

<%= pref_fields.select :textarea_font, textarea_font_options %>

<% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index eff0ea812..510968bd9 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -367,6 +367,7 @@ en: 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 field_default_status: Default status field_users_visibility: Users visibility field_time_entries_visibility: Time logs visibility @@ -534,6 +535,7 @@ en: 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 permission_import_issues: Import issues project_module_issue_tracking: Issue tracking @@ -1086,6 +1088,7 @@ en: button_delete_my_account: Delete my account button_close: Close button_reopen: Reopen + button_rollback: Rollback button_import: Import button_filter: Filter button_actions: Actions diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index a5b23c3d5..60e5c7fa6 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -1023,6 +1023,7 @@ pt-BR: notice_issue_update_conflict: A tarefa foi atualizada por um outro usuário, enquanto você estava editando. text_issue_conflict_resolution_cancel: Descartar todas as minhas mudanças e reexibir %{link} permission_manage_related_issues: Gerenciar tarefas relacionadas + permission_rollback_issue_notes: Desfazer mudanças nas tarefas field_auth_source_ldap_filter: Filtro LDAP label_search_for_watchers: Procurar por outros observadores para adicionar notice_account_deleted: Sua conta foi excluída permanentemente. @@ -1040,6 +1041,7 @@ pt-BR: label_show_closed_projects: Visualizar projetos fechados button_close: Fechar button_reopen: Reabrir + button_rollback: Desfazer project_status_active: ativo project_status_closed: fechado project_status_archived: arquivado @@ -1100,6 +1102,7 @@ pt-BR: label_visibility_roles: para os papéis label_visibility_public: para qualquer usuário field_must_change_passwd: É necessário alterar sua senha na próxima vez que tentar acessar sua conta + field_hide_rolled_back_issue_notes: Esconder mudanças desfeitas notice_new_password_must_be_different: A nova senha deve ser diferente da senha atual setting_mail_handler_excluded_filenames: Exclui anexos por nome text_convert_available: Conversor ImageMagick disponível (opcional) diff --git a/config/routes.rb b/config/routes.rb index be9838aea..044df6a87 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -50,6 +50,7 @@ Rails.application.routes.draw do resources :journals, :only => [:edit, :update] do member do get 'diff' + post 'rollback' end end diff --git a/db/migrate/20140417000000_add_journals_rolled_back.rb b/db/migrate/20140417000000_add_journals_rolled_back.rb new file mode 100644 index 000000000..443bef413 --- /dev/null +++ b/db/migrate/20140417000000_add_journals_rolled_back.rb @@ -0,0 +1,9 @@ +class AddJournalsRolledBack < ActiveRecord::Migration[4.2] + def up + add_column :journals, :rolled_back, :boolean, :default => false + end + + def down + remove_column :journals, :rolled_back + end +end diff --git a/lib/redmine.rb b/lib/redmine.rb index 73bff0ecf..ca09db056 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -107,8 +107,9 @@ Redmine::AccessControl.map do |map| map.permission :set_issues_private, {} map.permission :set_own_issues_private, {}, :require => :loggedin map.permission :add_issue_notes, {:issues => [:edit, :update], :journals => [:new], :attachments => :upload} - map.permission :edit_issue_notes, {:journals => [:edit, :update]}, :require => :loggedin - map.permission :edit_own_issue_notes, {:journals => [:edit, :update]}, :require => :loggedin + map.permission :edit_issue_notes, {:journals => [:edit, :update, :rollback]}, :require => :loggedin + map.permission :edit_own_issue_notes, {:journals => [:edit, :update, :rollback]}, :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 :delete_issues, {:issues => :destroy}, :require => :member -- 2.17.1