Project

General

Profile

Patch #7610 » 0001-Rollback-functionality.patch

For Redmine 4.0 - Frederico Camara, 2019-10-31 21:15

View differences:

app/controllers/journals_controller.rb
16 16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 17

  
18 18
class JournalsController < ApplicationController
19
  before_action :find_journal, :only => [:edit, :update, :diff]
19
  before_action :find_journal, :only => [:edit, :update, :diff, :rollback]
20 20
  before_action :find_issue, :only => [:new]
21 21
  before_action :find_optional_project, :only => [:index]
22
  before_action :authorize, :only => [:new, :edit, :update, :diff]
22
  before_action :authorize, :only => [:new, :edit, :update, :diff, :rollback]
23 23
  accept_rss_auth :index
24 24
  menu_item :issues
25 25

  
......
96 96
    end
97 97
  end
98 98

  
99
  def rollback
100
    Rails.logger.warn("FWH: #{@journal}")
101
    (render_403; return false) unless @journal.can_rollback?(User.current)
102
    @journal.rollback
103
    respond_to do |format|
104
      format.js
105
    end
106
  end
107

  
99 108
  private
100 109

  
101 110
  def find_journal
app/helpers/journals_helper.rb
28 28
  # Returns the action links for an issue journal
29 29
  def render_journal_actions(issue, journal, options={})
30 30
    links = []
31
    if journal.notes.present?
31
    if journal.last_valid_journal?(issue.journals) & journal.can_rollback?
32
      links << link_to(l(:button_cancel),
33
                       rollback_journal_path(journal),
34
                       :remote => true,
35
                       :method => 'post', :data => {:confirm => l(:text_are_you_sure)},
36
                       :title => l(:button_rollback),
37
                       :class => 'icon-only icon-cancel'
38
                      )
39
    end
40
    if journal.notes.present? & !journal.rolled_back?
32 41
      if options[:reply_links]
33 42
        links << link_to(l(:button_quote),
34 43
                         quoted_issue_path(issue, :journal_id => journal),
......
49 58
        links << link_to(l(:button_delete),
50 59
                         journal_path(journal, :journal => {:notes => ""}),
51 60
                         :remote => true,
52
                         :method => 'put', :data => {:confirm => l(:text_are_you_sure)}, 
61
                         :method => 'put', :data => {:confirm => l(:text_are_you_sure)},
53 62
                         :title => l(:button_delete),
54 63
                         :class => 'icon-only icon-del'
55 64
                        )
app/models/issue.rb
883 883
    end
884 884
  end
885 885

  
886
  # rollback the changes in a journal, the journal is destroyed on success
887
  def rollback(journal)
888
    # only allow rollback of journal details for the last journal
889
    (errors.add :base, l(:notice_locking_conflict); return) if !journal.last_valid_journal?(journals)
890

  
891
    # avoid the creation of journals during rollback (see 'attachment removed')
892
    @rolling_back = true
893

  
894
    # roll back each change detailed by the journal
895
    journal.details.each do |d|
896
      case d.property
897
        # issue attribute change
898
        when 'attr'; send "#{d.prop_key}=", d.old_value
899
        # rollback custom field change
900
        when 'cf'; custom_field_values.each {|v| v.value = d.old_value if v.custom_field_id == d.prop_key.to_i}
901
        # remove any added attachments (we can't recover removed attachments)
902
        when 'attachment'; attachments.each {|v| attachments.delete(v) if v.id == d.prop_key.to_i}
903
      end
904
    end
905

  
906
    # allow the creation of journals again
907
    remove_instance_variable(:@rolling_back)
908

  
909
    # destroy the journal once we save the issue changes
910
    if save(:validate => false)
911
      journal.rolled_back = true
912
      journal.save
913
    end
914
  end
915

  
886 916
  # Return true if the issue is closed, otherwise false
887 917
  def closed?
888 918
    status.present? && status.is_closed?
app/models/journal.rb
114 114
    usr && usr.logged? && (usr.allowed_to?(:edit_issue_notes, project) || (self.user == usr && usr.allowed_to?(:edit_own_issue_notes, project)))
115 115
  end
116 116

  
117
  def last_valid_journal?(journals)
118
    self == journals.reject{|j| j.rolled_back}.sort_by(&:id).last
119
  end
120

  
121
  def can_rollback?(user = nil)
122
    user ||= User.current
123
    editable_by?(user) && (details.empty? || user.allowed_to?(:rollback_issue_notes, project))
124
  end
125

  
126
  def show?
127
    !self.rolled_back || User.current.pref.hide_rolled_back_issue_notes == '0'
128
  end
129

  
130
  # rollback the changes in a journal, the journal is destroyed on success
131
  def rollback
132
    # we could have details to rollback, so let the issue take care of this more complicated task
133
    journalized.rollback(self) ||
134
    # on failure, collect the error messages from the issue on failure
135
      (journalized.errors.full_messages.each {|msg| errors.add :base, msg}; false)
136
  end
137

  
117 138
  def project
118 139
    journalized.respond_to?(:project) ? journalized.project : nil
119 140
  end
app/models/user_preference.rb
79 79
  def warn_on_leaving_unsaved; self[:warn_on_leaving_unsaved] || '1'; end
80 80
  def warn_on_leaving_unsaved=(value); self[:warn_on_leaving_unsaved]=value; end
81 81

  
82
  def hide_rolled_back_issue_notes; self[:hide_rolled_back_issue_notes] || '0'; end
83
  def hide_rolled_back_issue_notes=(value); self[:hide_rolled_back_issue_notes]=value; end
84

  
82 85
  def no_self_notified; (self[:no_self_notified] == true || self[:no_self_notified] == '1'); end
83 86
  def no_self_notified=(value); self[:no_self_notified]=value; end
84 87

  
app/views/issues/_history.html.erb
2 2
<% for journal in journals %>
3 3
  <div id="change-<%= journal.id %>" class="<%= journal.css_classes %>">
4 4
    <div id="note-<%= journal.indice %>">
5
    <div class="contextual">
6
      <span class="journal-actions"><%= render_journal_actions(issue, journal, :reply_links => reply_links) %></span>
7
      <a href="#note-<%= journal.indice %>" class="journal-link">#<%= journal.indice %></a>
8
    </div>
9
    <h4>
10
      <%= avatar(journal.user, :size => "24") %>
11
      <%= authoring journal.created_on, journal.user, :label => :label_updated_time_by %>
12
      <%= render_private_notes_indicator(journal) %>
13
    </h4>
5
      <% if journal.show? %>
6
        <% if journal.rolled_back? %>
7
          <strike>
8
        <% end %>
9
        <div class="contextual">
10
          <span class="journal-actions"><%= render_journal_actions(issue, journal, :reply_links => reply_links) %></span>
11
          <a href="#note-<%= journal.indice %>" class="journal-link">#<%= journal.indice %></a>
12
        </div>
13
        <h4>
14
          <%= avatar(journal.user, :size => "24") %>
15
          <%= authoring journal.created_on, journal.user, :label => :label_updated_time_by %>
16
          <%= render_private_notes_indicator(journal) %>
17
        </h4>
14 18

  
15
    <% if journal.details.any? %>
16
    <ul class="details">
17
      <% details_to_strings(journal.visible_details).each do |string| %>
18
       <li><%= string %></li>
19
      <% end %>
20
    </ul>
21
    <% if Setting.thumbnails_enabled? && (thumbnail_attachments = journal_thumbnail_attachments(journal)).any? %>
22
      <div class="thumbnails">
23
        <% thumbnail_attachments.each do |attachment| %>
24
          <div><%= thumbnail_tag(attachment) %></div>
19
        <% if journal.details.any? %>
20
          <ul class="details">
21
            <% details_to_strings(journal.visible_details).each do |string| %>
22
              <li><%= string %></li>
23
            <% end %>
24
          </ul>
25
          <% if Setting.thumbnails_enabled? && (thumbnail_attachments = journal_thumbnail_attachments(journal)).any? %>
26
            <div class="thumbnails">
27
              <% thumbnail_attachments.each do |attachment| %>
28
                <div><%= thumbnail_tag(attachment) %></div>
29
              <% end %>
30
            </div>
31
          <% end %>
25 32
        <% end %>
26
      </div>
27
    <% end %>
28
    <% end %>
29
    <%= render_notes(issue, journal, :reply_links => reply_links) unless journal.notes.blank? %>
33
        <%= render_notes(issue, journal, :reply_links => reply_links) unless journal.notes.blank? %>
34
        <% if journal.rolled_back? %>
35
          </strike>
36
        <% end %>
37
      <% end %>
30 38
    </div>
31 39
  </div>
32
  <%= call_hook(:view_issues_history_journal_bottom, { :journal => journal }) %>
40
<%= call_hook(:view_issues_history_journal_bottom, { :journal => journal }) %>
41

  
33 42
<% end %>
34 43

  
35 44
<% heads_for_wiki_formatter if User.current.allowed_to?(:edit_issue_notes, issue.project) || User.current.allowed_to?(:edit_own_issue_notes, issue.project) %>
app/views/journals/rollback.js.erb
1
$('#history').html('<h3><%=l(:label_history)%></h3><%= escape_javascript(render :partial => 'issues/history', :locals => { :issue => @journal.issue, :journals => @journal.issue.journals }) %>');
app/views/users/_preferences.html.erb
3 3
<p><%= pref_fields.time_zone_select :time_zone, nil, :include_blank => true %></p>
4 4
<p><%= pref_fields.select :comments_sorting, [[l(:label_chronological_order), 'asc'], [l(:label_reverse_chronological_order), 'desc']] %></p>
5 5
<p><%= pref_fields.check_box :warn_on_leaving_unsaved %></p>
6
<p><%= pref_fields.check_box :hide_rolled_back_issue_notes %></p>
6 7
<p><%= pref_fields.select :textarea_font, textarea_font_options %></p>
7 8
<% end %>
config/locales/en.yml
367 367
  field_inherit_members: Inherit members
368 368
  field_generate_password: Generate password
369 369
  field_must_change_passwd: Must change password at next logon
370
  field_hide_rolled_back_issue_notes: Hide rolled-back issue notes
370 371
  field_default_status: Default status
371 372
  field_users_visibility: Users visibility
372 373
  field_time_entries_visibility: Time logs visibility
......
534 535
  permission_export_wiki_pages: Export wiki pages
535 536
  permission_manage_subtasks: Manage subtasks
536 537
  permission_manage_related_issues: Manage related issues
538
  permission_rollback_issue_notes: Rollback issue notes
537 539
  permission_import_issues: Import issues
538 540

  
539 541
  project_module_issue_tracking: Issue tracking
......
1086 1088
  button_delete_my_account: Delete my account
1087 1089
  button_close: Close
1088 1090
  button_reopen: Reopen
1091
  button_rollback: Rollback
1089 1092
  button_import: Import
1090 1093
  button_filter: Filter
1091 1094
  button_actions: Actions
config/locales/pt-BR.yml
1023 1023
  notice_issue_update_conflict: A tarefa foi atualizada por um outro usuário, enquanto você estava editando.
1024 1024
  text_issue_conflict_resolution_cancel: Descartar todas as minhas mudanças e reexibir %{link}
1025 1025
  permission_manage_related_issues: Gerenciar tarefas relacionadas
1026
  permission_rollback_issue_notes: Desfazer mudanças nas tarefas
1026 1027
  field_auth_source_ldap_filter: Filtro LDAP
1027 1028
  label_search_for_watchers: Procurar por outros observadores para adicionar
1028 1029
  notice_account_deleted: Sua conta foi excluída permanentemente.
......
1040 1041
  label_show_closed_projects: Visualizar projetos fechados
1041 1042
  button_close: Fechar
1042 1043
  button_reopen: Reabrir
1044
  button_rollback: Desfazer
1043 1045
  project_status_active: ativo
1044 1046
  project_status_closed: fechado
1045 1047
  project_status_archived: arquivado
......
1100 1102
  label_visibility_roles: para os papéis
1101 1103
  label_visibility_public: para qualquer usuário
1102 1104
  field_must_change_passwd: É necessário alterar sua senha na próxima vez que tentar acessar sua conta
1105
  field_hide_rolled_back_issue_notes: Esconder mudanças desfeitas
1103 1106
  notice_new_password_must_be_different: A nova senha deve ser diferente da senha atual
1104 1107
  setting_mail_handler_excluded_filenames: Exclui anexos por nome
1105 1108
  text_convert_available: Conversor ImageMagick disponível (opcional)
config/routes.rb
50 50
  resources :journals, :only => [:edit, :update] do
51 51
    member do
52 52
      get 'diff'
53
      post 'rollback'
53 54
    end
54 55
  end
55 56

  
db/migrate/20140417000000_add_journals_rolled_back.rb
1
class AddJournalsRolledBack < ActiveRecord::Migration[4.2]
2
  def up
3
    add_column :journals, :rolled_back, :boolean, :default => false
4
  end
5

  
6
  def down
7
    remove_column :journals, :rolled_back
8
  end
9
end
lib/redmine.rb
107 107
    map.permission :set_issues_private, {}
108 108
    map.permission :set_own_issues_private, {}, :require => :loggedin
109 109
    map.permission :add_issue_notes, {:issues => [:edit, :update], :journals => [:new], :attachments => :upload}
110
    map.permission :edit_issue_notes, {:journals => [:edit, :update]}, :require => :loggedin
111
    map.permission :edit_own_issue_notes, {:journals => [:edit, :update]}, :require => :loggedin
110
    map.permission :edit_issue_notes, {:journals => [:edit, :update, :rollback]}, :require => :loggedin
111
    map.permission :edit_own_issue_notes, {:journals => [:edit, :update, :rollback]}, :require => :loggedin
112
    map.permission :rollback_issue_notes, {:journals => [:rollback]}
112 113
    map.permission :view_private_notes, {}, :read => true, :require => :member
113 114
    map.permission :set_notes_private, {}, :require => :member
114 115
    map.permission :delete_issues, {:issues => :destroy}, :require => :member
(14-14/15)