Patch #7610 » 0003-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 | 
    (render_403; return false) unless @journal.can_rollback?(User.current)  | 
|
| 101 | 
    if @journal.rollback  | 
|
| 102 | 
    flash[:notice] = l(:notice_successful_update)  | 
|
| 103 | 
    else  | 
|
| 104 | 
    # can't seem to bring in the helper method 'error_messages_for'  | 
|
| 105 | 
    # and injecting it into show.rhtml doesn't seem to work, since  | 
|
| 106 | 
    # the @issue loses the errors on redirect (due to issue reload)  | 
|
| 107 | 
          flash[:error] = "<ul>" + @journal.errors.full_messages.map {|msg| "<li>" + ERB::Util.html_escape(msg) + "</li>"}.join + "</ul>"
   | 
|
| 108 | 
    end  | 
|
| 109 | 
    respond_to do |format|  | 
|
| 110 | 
          format.html { redirect_to :controller => 'issues', :action => 'show', :id => @journal.journalized_id, :anchor => "note-#{@journal.issue.journals.index(@journal) + 1}" }
   | 
|
| 111 | 
          format.api  { render_validation_errors(@issue) }
   | 
|
| 112 | 
    end  | 
|
| 113 | 
    end  | 
|
| 114 | ||
| 99 | 115 | 
    private  | 
| 100 | 116 | |
| 101 | 117 | 
    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 | 
                           :method => 'post', :data => {:confirm => l(:text_are_you_sure)},
   | 
|
| 35 | 
    :title => l(:button_rollback),  | 
|
| 36 | 
    :class => 'icon-only icon-cancel'  | 
|
| 37 | 
    )  | 
|
| 38 | 
    end  | 
|
| 39 | 
    if journal.notes.present? & !journal.rolled_back?  | 
|
| 32 | 40 | 
    if options[:reply_links]  | 
| 33 | 41 | 
    links << link_to(l(:button_quote),  | 
| 34 | 42 | 
    quoted_issue_path(issue, :journal_id => journal),  | 
| ... | ... | |
| 49 | 57 | 
    links << link_to(l(:button_delete),  | 
| 50 | 58 | 
                             journal_path(journal, :journal => {:notes => ""}),
   | 
| 51 | 59 | 
    :remote => true,  | 
| 52 | 
                             :method => 'put', :data => {:confirm => l(:text_are_you_sure)}, 
   | 
|
| 60 | 
                             :method => 'put', :data => {:confirm => l(:text_are_you_sure)},
   | 
|
| 53 | 61 | 
    :title => l(:button_delete),  | 
| 54 | 62 | 
    :class => 'icon-only icon-del'  | 
| 55 | 63 | 
    )  | 
| 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 | ||
|---|---|---|
| 29 | 29 | 
    'time_zone',  | 
| 30 | 30 | 
    'comments_sorting',  | 
| 31 | 31 | 
    'warn_on_leaving_unsaved',  | 
| 32 | 
    'hide_rolled_back_issue_notes',  | 
|
| 32 | 33 | 
    'no_self_notified',  | 
| 33 | 34 | 
    'textarea_font'  | 
| 34 | 35 | |
| ... | ... | |
| 79 | 80 | 
    def warn_on_leaving_unsaved; self[:warn_on_leaving_unsaved] || '1'; end  | 
| 80 | 81 | 
    def warn_on_leaving_unsaved=(value); self[:warn_on_leaving_unsaved]=value; end  | 
| 81 | 82 | |
| 83 | 
    def hide_rolled_back_issue_notes; self[:hide_rolled_back_issue_notes] || '0'; end  | 
|
| 84 | 
    def hide_rolled_back_issue_notes=(value); self[:hide_rolled_back_issue_notes]=value; end  | 
|
| 85 | ||
| 82 | 86 | 
    def no_self_notified; (self[:no_self_notified] == true || self[:no_self_notified] == '1'); end  | 
| 83 | 87 | 
    def no_self_notified=(value); self[:no_self_notified]=value; end  | 
| 84 | 88 | |
| 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/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
   | 
- « Previous
 - 1
 - …
 - 13
 - 14
 - 15
 - Next »