Patch #7610 » 0001-Rollback-functionality.patch
| 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
|