Commit dabe5caa authored by Eric Davis's avatar Eric Davis

Merge branch 'ticket/unstable/123-journalized' into unstable

parents bf90848a f79e209d
...@@ -17,8 +17,7 @@ class IssueMovesController < ApplicationController ...@@ -17,8 +17,7 @@ class IssueMovesController < ApplicationController
moved_issues = [] moved_issues = []
@issues.each do |issue| @issues.each do |issue|
issue.reload issue.reload
issue.init_journal(User.current) issue.init_journal(User.current, @notes || "")
issue.current_journal.notes = @notes if @notes.present?
call_hook(:controller_issues_move_before_save, { :params => params, :issue => issue, :target_project => @target_project, :copy => !!@copy }) call_hook(:controller_issues_move_before_save, { :params => params, :issue => issue, :target_project => @target_project, :copy => !!@copy })
if r = issue.move_to_project(@target_project, new_tracker, {:copy => @copy, :attributes => extract_changed_attributes_for_move(params)}) if r = issue.move_to_project(@target_project, new_tracker, {:copy => @copy, :attributes => extract_changed_attributes_for_move(params)})
moved_issues << r moved_issues << r
......
...@@ -31,6 +31,9 @@ class IssuesController < ApplicationController ...@@ -31,6 +31,9 @@ class IssuesController < ApplicationController
rescue_from Query::StatementInvalid, :with => :query_statement_invalid rescue_from Query::StatementInvalid, :with => :query_statement_invalid
helper :journals
include JournalsHelper
helper :projects
include ProjectsHelper include ProjectsHelper
include CustomFieldsHelper include CustomFieldsHelper
include IssueRelationsHelper include IssueRelationsHelper
...@@ -92,8 +95,7 @@ class IssuesController < ApplicationController ...@@ -92,8 +95,7 @@ class IssuesController < ApplicationController
end end
def show def show
@journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC") @journals = @issue.journals.find(:all, :include => [:user], :order => "#{Journal.table_name}.created_at ASC")
@journals.each_with_index {|j,i| j.indice = i+1}
@journals.reverse! if User.current.wants_comments_in_reverse_order? @journals.reverse! if User.current.wants_comments_in_reverse_order?
@changesets = @issue.changesets.visible.all @changesets = @issue.changesets.visible.all
@changesets.reverse! if User.current.wants_comments_in_reverse_order? @changesets.reverse! if User.current.wants_comments_in_reverse_order?
...@@ -144,6 +146,7 @@ class IssuesController < ApplicationController ...@@ -144,6 +146,7 @@ class IssuesController < ApplicationController
end end
def edit def edit
return render_reply(@journal) if @journal
update_issue_from_params update_issue_from_params
@journal = @issue.current_journal @journal = @issue.current_journal
...@@ -159,7 +162,7 @@ class IssuesController < ApplicationController ...@@ -159,7 +162,7 @@ class IssuesController < ApplicationController
JournalObserver.instance.send_notification = params[:send_notification] == '0' ? false : true JournalObserver.instance.send_notification = params[:send_notification] == '0' ? false : true
if @issue.save_issue_with_child_records(params, @time_entry) if @issue.save_issue_with_child_records(params, @time_entry)
render_attachment_warning_if_needed(@issue) render_attachment_warning_if_needed(@issue)
flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record? flash[:notice] = l(:notice_successful_update) unless @issue.current_journal == @journal
respond_to do |format| respond_to do |format|
format.html { redirect_back_or_default({:action => 'show', :id => @issue}) } format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
...@@ -167,7 +170,7 @@ class IssuesController < ApplicationController ...@@ -167,7 +170,7 @@ class IssuesController < ApplicationController
end end
else else
render_attachment_warning_if_needed(@issue) render_attachment_warning_if_needed(@issue)
flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record? flash[:notice] = l(:notice_successful_update) unless @issue.current_journal == @journal
@journal = @issue.current_journal @journal = @issue.current_journal
respond_to do |format| respond_to do |format|
...@@ -268,6 +271,7 @@ private ...@@ -268,6 +271,7 @@ private
@notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil) @notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
@issue.init_journal(User.current, @notes) @issue.init_journal(User.current, @notes)
@issue.safe_attributes = params[:issue] @issue.safe_attributes = params[:issue]
@journal = @issue.current_journal
end end
# TODO: Refactor, lots of extra code in here # TODO: Refactor, lots of extra code in here
......
# Redmine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class JournalsController < ApplicationController
before_filter :find_journal, :only => [:edit, :diff]
before_filter :find_issue, :only => [:new]
before_filter :find_optional_project, :only => [:index]
before_filter :authorize, :only => [:new, :edit, :diff]
accept_key_auth :index
menu_item :issues
include QueriesHelper
include SortHelper
def index
retrieve_query
sort_init 'id', 'desc'
sort_update(@query.sortable_columns)
if @query.valid?
@journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
:limit => 25)
end
@title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
render :layout => false, :content_type => 'application/atom+xml'
rescue ActiveRecord::RecordNotFound
render_404
end
def diff
@issue = @journal.issue
if params[:detail_id].present?
@detail = @journal.details.find_by_id(params[:detail_id])
else
@detail = @journal.details.detect {|d| d.prop_key == 'description'}
end
(render_404; return false) unless @issue && @detail
@diff = Redmine::Helpers::Diff.new(@detail.value, @detail.old_value)
end
def new
journal = Journal.find(params[:journal_id]) if params[:journal_id]
if journal
user = journal.user
text = journal.notes
else
user = @issue.author
text = @issue.description
end
# Replaces pre blocks with [...]
text = text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]')
content = "#{ll(Setting.default_language, :text_user_wrote, user)}\n> "
content << text.gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n"
render(:update) { |page|
page.<< "$('notes').value = \"#{escape_javascript content}\";"
page.show 'update'
page << "Form.Element.focus('notes');"
page << "Element.scrollTo('update');"
page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
}
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?
call_hook(:controller_journals_edit_post, { :journal => @journal, :params => params})
respond_to do |format|
format.html { redirect_to :controller => 'issues', :action => 'show', :id => @journal.journalized_id }
format.js { render :action => 'update' }
end
else
respond_to do |format|
format.html {
# TODO: implement non-JS journal update
render :nothing => true
}
format.js
end
end
end
private
def find_journal
@journal = Journal.find(params[:id])
@project = @journal.journalized.project
rescue ActiveRecord::RecordNotFound
render_404
end
# TODO: duplicated in IssuesController
def find_issue
@issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
@project = @issue.project
rescue ActiveRecord::RecordNotFound
render_404
end
end
...@@ -97,7 +97,7 @@ class WikiController < ApplicationController ...@@ -97,7 +97,7 @@ class WikiController < ApplicationController
@content.comments = nil @content.comments = nil
# To prevent StaleObjectError exception when reverting to a previous version # To prevent StaleObjectError exception when reverting to a previous version
@content.version = @page.content.version @content.lock_version = @page.content.lock_version
end end
verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed } verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
...@@ -119,6 +119,7 @@ class WikiController < ApplicationController ...@@ -119,6 +119,7 @@ class WikiController < ApplicationController
redirect_to :action => 'show', :project_id => @project, :id => @page.title redirect_to :action => 'show', :project_id => @project, :id => @page.title
return return
end end
params[:content].delete(:version) # The version count is automatically increased
@content.attributes = params[:content] @content.attributes = params[:content]
@content.author = User.current @content.author = User.current
# if page is new @page.save will also save content, but not if page isn't a new record # if page is new @page.save will also save content, but not if page isn't a new record
...@@ -160,7 +161,7 @@ class WikiController < ApplicationController ...@@ -160,7 +161,7 @@ class WikiController < ApplicationController
@version_pages = Paginator.new self, @version_count, per_page_option, params['p'] @version_pages = Paginator.new self, @version_count, per_page_option, params['p']
# don't load text # don't load text
@versions = @page.content.versions.find :all, @versions = @page.content.versions.find :all,
:select => "id, author_id, comments, updated_on, version", :select => "id, user_id, notes, created_at, version",
:order => 'version DESC', :order => 'version DESC',
:limit => @version_pages.items_per_page + 1, :limit => @version_pages.items_per_page + 1,
:offset => @version_pages.current.offset :offset => @version_pages.current.offset
......
...@@ -19,28 +19,43 @@ require "digest/md5" ...@@ -19,28 +19,43 @@ require "digest/md5"
class Attachment < ActiveRecord::Base class Attachment < ActiveRecord::Base
belongs_to :container, :polymorphic => true belongs_to :container, :polymorphic => true
belongs_to :author, :class_name => "User", :foreign_key => "author_id"
# FIXME: Remove these once the Versions, Documents and Projects themselves can provide file events
belongs_to :version, :foreign_key => "container_id"
belongs_to :document, :foreign_key => "container_id"
belongs_to :author, :class_name => "User", :foreign_key => "author_id"
validates_presence_of :container, :filename, :author validates_presence_of :container, :filename, :author
validates_length_of :filename, :maximum => 255 validates_length_of :filename, :maximum => 255
validates_length_of :disk_filename, :maximum => 255 validates_length_of :disk_filename, :maximum => 255
acts_as_event :title => :filename, acts_as_journalized :event_title => :filename,
:url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}} :event_url => (Proc.new do |o|
{ :controller => 'attachments', :action => 'download',
:id => o.journaled_id, :filename => o.filename }
end),
:activity_type => 'files',
:activity_permission => :view_files,
:activity_find_options => { :include => { :version => :project } }
acts_as_activity_provider :type => 'files', acts_as_activity :type => 'documents', :permission => :view_documents,
:permission => :view_files, :find_options => { :include => { :document => :project } }
:author_key => :author_id,
:find_options => {:select => "#{Attachment.table_name}.*", # This method is called on save by the AttachmentJournal in order to
:joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " + # decide which kind of activity we are dealing with. When that activity
"LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id OR ( #{Attachment.table_name}.container_type='Project' AND #{Attachment.table_name}.container_id = #{Project.table_name}.id )"} # is retrieved later, we don't need to check the container_type in
# SQL anymore as that will be just the one we have specified here.
acts_as_activity_provider :type => 'documents', def activity_type
:permission => :view_documents, case container_type
:author_key => :author_id, when "Document"
:find_options => {:select => "#{Attachment.table_name}.*", "documents"
:joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " + when "Version"
"LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"} "files"
else
super
end
end
cattr_accessor :storage_path cattr_accessor :storage_path
@@storage_path = Redmine::Configuration['attachments_storage_path'] || "#{RAILS_ROOT}/files" @@storage_path = Redmine::Configuration['attachments_storage_path'] || "#{RAILS_ROOT}/files"
......
...@@ -23,19 +23,17 @@ class Changeset < ActiveRecord::Base ...@@ -23,19 +23,17 @@ class Changeset < ActiveRecord::Base
has_many :changes, :dependent => :delete_all has_many :changes, :dependent => :delete_all
has_and_belongs_to_many :issues has_and_belongs_to_many :issues
acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.format_identifier}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))}, acts_as_journalized :event_title => Proc.new {|o| "#{l(:label_revision)} #{o.format_identifier}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
:description => :long_comments, :event_description => :long_comments,
:datetime => :committed_on, :event_datetime => :committed_on,
:url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.identifier}} :event_url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.revision}},
:activity_timestamp => "#{table_name}.committed_on",
:activity_find_options => {:include => [:user, {:repository => :project}]}
acts_as_searchable :columns => 'comments', acts_as_searchable :columns => 'comments',
:include => {:repository => :project}, :include => {:repository => :project},
:project_key => "#{Repository.table_name}.project_id", :project_key => "#{Repository.table_name}.project_id",
:date_column => 'committed_on' :date_column => 'committed_on'
acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
:author_key => :user_id,
:find_options => {:include => [:user, {:repository => :project}]}
validates_presence_of :repository_id, :revision, :committed_on, :commit_date validates_presence_of :repository_id, :revision, :committed_on, :commit_date
validates_uniqueness_of :revision, :scope => :repository_id validates_uniqueness_of :revision, :scope => :repository_id
...@@ -193,7 +191,7 @@ class Changeset < ActiveRecord::Base ...@@ -193,7 +191,7 @@ class Changeset < ActiveRecord::Base
# don't change the status is the issue is closed # don't change the status is the issue is closed
return if issue.status && issue.status.is_closed? return if issue.status && issue.status.is_closed?
journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag)) issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag))
issue.status = status issue.status = status
unless Setting.commit_fix_done_ratio.blank? unless Setting.commit_fix_done_ratio.blank?
issue.done_ratio = Setting.commit_fix_done_ratio.to_i issue.done_ratio = Setting.commit_fix_done_ratio.to_i
......
...@@ -20,11 +20,13 @@ class Document < ActiveRecord::Base ...@@ -20,11 +20,13 @@ class Document < ActiveRecord::Base
belongs_to :category, :class_name => "DocumentCategory", :foreign_key => "category_id" belongs_to :category, :class_name => "DocumentCategory", :foreign_key => "category_id"
acts_as_attachable :delete_permission => :manage_documents acts_as_attachable :delete_permission => :manage_documents
acts_as_journalized :event_title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
:event_url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.journaled_id}},
:event_author => (Proc.new do |o|
o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC").try(:author)
end)
acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
:author => Proc.new {|o| (a = o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC")) ? a.author : nil },
:url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}}
acts_as_activity_provider :find_options => {:include => :project}
validates_presence_of :project, :title, :category validates_presence_of :project, :title, :category
validates_length_of :title, :maximum => 60 validates_length_of :title, :maximum => 60
......
...@@ -27,7 +27,6 @@ class Issue < ActiveRecord::Base ...@@ -27,7 +27,6 @@ class Issue < ActiveRecord::Base
belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id' belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id' belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
has_many :journals, :as => :journalized, :dependent => :destroy
has_many :time_entries, :dependent => :delete_all has_many :time_entries, :dependent => :delete_all
has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC" has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
...@@ -38,21 +37,25 @@ class Issue < ActiveRecord::Base ...@@ -38,21 +37,25 @@ class Issue < ActiveRecord::Base
acts_as_attachable :after_remove => :attachment_removed acts_as_attachable :after_remove => :attachment_removed
acts_as_customizable acts_as_customizable
acts_as_watchable acts_as_watchable
acts_as_journalized :event_title => Proc.new {|o| "#{o.tracker.name} ##{o.journaled_id} (#{o.status}): #{o.subject}"},
:event_type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '')}
register_on_journal_formatter(:id, 'parent_id')
register_on_journal_formatter(:named_association, 'project_id', 'status_id', 'tracker_id', 'assigned_to_id',
'priority_id', 'category_id', 'fixed_version_id')
register_on_journal_formatter(:fraction, 'estimated_hours')
register_on_journal_formatter(:decimal, 'done_ratio')
register_on_journal_formatter(:datetime, 'due_date', 'start_date')
register_on_journal_formatter(:plaintext, 'subject', 'description')
acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"], acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
:include => [:project, :journals], :include => [:project, :journals],
# sort by id so that limited eager loading doesn't break with postgresql # sort by id so that limited eager loading doesn't break with postgresql
:order_column => "#{table_name}.id" :order_column => "#{table_name}.id"
acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
:url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
:type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
:author_key => :author_id
DONE_RATIO_OPTIONS = %w(issue_field issue_status) DONE_RATIO_OPTIONS = %w(issue_field issue_status)
attr_reader :current_journal
validates_presence_of :subject, :priority, :project, :tracker, :author, :status validates_presence_of :subject, :priority, :project, :tracker, :author, :status
validates_length_of :subject, :maximum => 255 validates_length_of :subject, :maximum => 255
...@@ -83,7 +86,7 @@ class Issue < ActiveRecord::Base ...@@ -83,7 +86,7 @@ class Issue < ActiveRecord::Base
before_create :default_assign before_create :default_assign
before_save :close_duplicates, :update_done_ratio_from_issue_status before_save :close_duplicates, :update_done_ratio_from_issue_status
after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes
after_destroy :update_parent_attributes after_destroy :update_parent_attributes
# Returns a SQL conditions string used to find all issues visible by the specified user # Returns a SQL conditions string used to find all issues visible by the specified user
...@@ -346,15 +349,11 @@ class Issue < ActiveRecord::Base ...@@ -346,15 +349,11 @@ class Issue < ActiveRecord::Base
end end
end end
def init_journal(user, notes = "") # Callback on attachment deletion
@current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes) def attachment_removed(obj)
@issue_before_change = self.clone init_journal(User.current)
@issue_before_change.status = self.status create_journal
@custom_values_before_change = {} last_journal.update_attribute(:changes, {"attachments_" + obj.id.to_s => [obj.filename, nil]}.to_yaml)
self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
# Make sure updated_on is updated when adding a note.
updated_on_will_change!
@current_journal
end end
# Return true if the issue is closed, otherwise false # Return true if the issue is closed, otherwise false
...@@ -556,13 +555,12 @@ class Issue < ActiveRecord::Base ...@@ -556,13 +555,12 @@ class Issue < ActiveRecord::Base
if valid? if valid?
attachments = Attachment.attach_files(self, params[:attachments]) attachments = Attachment.attach_files(self, params[:attachments])
attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
# TODO: Rename hook # TODO: Rename hook
Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal}) Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => current_journal})
begin begin
if save if save
# TODO: Rename hook # TODO: Rename hook
Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal}) Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => current_journal})
else else
raise ActiveRecord::Rollback raise ActiveRecord::Rollback
end end
...@@ -777,22 +775,12 @@ class Issue < ActiveRecord::Base ...@@ -777,22 +775,12 @@ class Issue < ActiveRecord::Base
).each do |issue| ).each do |issue|
next if issue.project.nil? || issue.fixed_version.nil? next if issue.project.nil? || issue.fixed_version.nil?
unless issue.project.shared_versions.include?(issue.fixed_version) unless issue.project.shared_versions.include?(issue.fixed_version)
issue.init_journal(User.current)
issue.fixed_version = nil issue.fixed_version = nil
issue.save issue.save
end end
end end
end end
# Callback on attachment deletion
def attachment_removed(obj)
journal = init_journal(User.current)
journal.details << JournalDetail.new(:property => 'attachment',
:prop_key => obj.id,
:old_value => obj.filename)
journal.save
end
# Default assignment based on category # Default assignment based on category
def default_assign def default_assign
if assigned_to.nil? && category && category.assigned_to if assigned_to.nil? && category && category.assigned_to
...@@ -817,40 +805,14 @@ class Issue < ActiveRecord::Base ...@@ -817,40 +805,14 @@ class Issue < ActiveRecord::Base
duplicate.reload duplicate.reload
# Don't re-close it if it's already closed # Don't re-close it if it's already closed
next if duplicate.closed? next if duplicate.closed?
# Same user and notes # Implicitely creates a new journal
if @current_journal
duplicate.init_journal(@current_journal.user, @current_journal.notes)
end
duplicate.update_attribute :status, self.status duplicate.update_attribute :status, self.status
# Same user and notes
duplicate.journals.last.user = current_journal.user
duplicate.journals.last.notes = current_journal.notes
end end
end end
end end
# Saves the changes in a Journal
# Called after_save
def create_journal
if @current_journal
# attributes changes
(Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
@current_journal.details << JournalDetail.new(:property => 'attr',
:prop_key => c,
:old_value => @issue_before_change.send(c),
:value => send(c)) unless send(c)==@issue_before_change.send(c)
}
# custom fields changes
custom_values.each {|c|
next if (@custom_values_before_change[c.custom_field_id]==c.value ||
(@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
@current_journal.details << JournalDetail.new(:property => 'cf',
:prop_key => c.custom_field_id,
:old_value => @custom_values_before_change[c.custom_field_id],
:value => c.value)
}
@current_journal.save
# reset current journal
init_journal @current_journal.user, @current_journal.notes
end
end
# Query generator for selecting groups of issue counts for a project # Query generator for selecting groups of issue counts for a project
# based on specific criteria # based on specific criteria
...@@ -880,4 +842,13 @@ class Issue < ActiveRecord::Base ...@@ -880,4 +842,13 @@ class Issue < ActiveRecord::Base
end end
IssueJournal.class_eval do
# Shortcut
def new_status
if details.keys.include? 'status_id'
(newval = details['status_id'].last) ? IssueStatus.find_by_id(newval.to_i) : nil
end
end
end
end end
# Redmine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class Journal < ActiveRecord::Base
belongs_to :journalized, :polymorphic => true
# added as a quick fix to allow eager loading of the polymorphic association
# since always associated to an issue, for now
belongs_to :issue, :foreign_key => :journalized_id
belongs_to :user
has_many :details, :class_name => "JournalDetail", :dependent => :delete_all
attr_accessor :indice
acts_as_event :title => Proc.new {|o| status = ((s = o.new_status) ? " (#{s})" : nil); "#{o.issue.tracker} ##{o.issue.id}#{status}: #{o.issue.subject}" },
:description => :notes,
:author => :user,
:type => Proc.new {|o| (s = o.new_status) ? (s.is_closed? ? 'issue-closed' : 'issue-edit') : 'issue-note' },
:url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.id, :anchor => "change-#{o.id}"}}
acts_as_activity_provider :type => 'issues',
:permission => :view_issues,
:author_key => :user_id,
:find_options => {:include => [{:issue => :project}, :details, :user],
:conditions => "#{Journal.table_name}.journalized_type = 'Issue' AND" +
" (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')"}
named_scope :visible, lambda {|*args| {
:include => {:issue => :project},
:conditions => Issue.visible_condition(args.first || User.current)
}}
def save(*args)
# Do not save an empty journal
(details.empty? && notes.blank?) ? false : super
end
# Returns the new status if the journal contains a status change, otherwise nil
def new_status
c = details.detect {|detail| detail.prop_key == 'status_id'}
(c && c.value) ? IssueStatus.find_by_id(c.value.to_i) : nil
end
def new_value_for(prop)
c = details.detect {|detail| detail.prop_key == prop}
c ? c.value : nil
end
def editable_by?(usr)
usr && usr.logged? && (usr.allowed_to?(:edit_issue_notes, project) || (self.user == usr && usr.allowed_to?(:edit_own_issue_notes, project)))
end
def project
journalized.respond_to?(:project) ? journalized.project : nil
end
def attachments
journalized.respond_to?(:attachments) ? journalized.attachments : nil
end
# Returns a string of css classes
def css_classes
s = 'journal'
s << ' has-notes' unless notes.blank?
s << ' has-details' unless details.blank?
s
end
end
...@@ -159,21 +159,20 @@ class MailHandler < ActionMailer::Base ...@@ -159,21 +159,20 @@ class MailHandler < ActionMailer::Base
# ignore CLI-supplied defaults for new issues # ignore CLI-supplied defaults for new issues
@@handler_options[:issue].clear @@handler_options[:issue].clear
journal = issue.init_journal(user)
issue.safe_attributes = issue_attributes_from_keywords(issue) issue.safe_attributes = issue_attributes_from_keywords(issue)
issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)} issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
journal.notes = cleaned_up_text_body issue.init_journal(user, cleaned_up_text_body)
add_attachments(issue) add_attachments(issue)
issue.save! issue.save!
logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
journal issue.last_journal
end end
# Reply will be added to the issue # Reply will be added to the issue
def receive_journal_reply(journal_id) def receive_journal_reply(journal_id)
journal = Journal.find_by_id(journal_id) journal = Journal.find_by_id(journal_id)
if journal && journal.journalized_type == 'Issue' if journal and journal.journaled.is_a? Issue
receive_issue_reply(journal.journalized_id) receive_issue_reply(journal.journaled_id)
end end
end end
......
...@@ -21,6 +21,7 @@ class Mailer < ActionMailer::Base ...@@ -21,6 +21,7 @@ class Mailer < ActionMailer::Base
layout 'mailer' layout 'mailer'
helper :application helper :application
helper :issues helper :issues
helper :journals
helper :custom_fields helper :custom_fields
include ActionController::UrlWriter include ActionController::UrlWriter
...@@ -58,7 +59,7 @@ class Mailer < ActionMailer::Base ...@@ -58,7 +59,7 @@ class Mailer < ActionMailer::Base
# issue_edit(journal) => tmail object # issue_edit(journal) => tmail object
# Mailer.deliver_issue_edit(journal) => sends an email to issue recipients # Mailer.deliver_issue_edit(journal) => sends an email to issue recipients
def issue_edit(journal) def issue_edit(journal)
issue = journal.journalized.reload issue = journal.journaled.reload
redmine_headers 'Project' => issue.project.identifier, redmine_headers 'Project' => issue.project.identifier,
'Issue-Id' => issue.id, 'Issue-Id' => issue.id,
'Issue-Author' => issue.author.login, 'Issue-Author' => issue.author.login,
...@@ -71,7 +72,7 @@ class Mailer < ActionMailer::Base ...@@ -71,7 +72,7 @@ class Mailer < ActionMailer::Base
# Watchers in cc # Watchers in cc
cc(issue.watcher_recipients - @recipients) cc(issue.watcher_recipients - @recipients)
s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] " s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
s << "(#{issue.status.name}) " if journal.new_value_for('status_id') s << "(#{issue.status.name}) " if journal.details['status_id']
s << issue.subject s << issue.subject
subject s subject s
body :issue => issue, body :issue => issue,
...@@ -188,7 +189,7 @@ class Mailer < ActionMailer::Base ...@@ -188,7 +189,7 @@ class Mailer < ActionMailer::Base
cc((message.root.watcher_recipients + message.board.watcher_recipients).uniq - @recipients) cc((message.root.watcher_recipients + message.board.watcher_recipients).uniq - @recipients)
subject "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}" subject "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}"
body :message => message, body :message => message,
:message_url => url_for(message.event_url) :message_url => url_for(message.last_journal.event_url)
render_multipart('message_posted', body) render_multipart('message_posted', body)
end end
......
...@@ -22,18 +22,24 @@ class Message < ActiveRecord::Base ...@@ -22,18 +22,24 @@ class Message < ActiveRecord::Base
acts_as_attachable acts_as_attachable
belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id' belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id'
acts_as_journalized :event_title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},
:event_description => :content,
:event_type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'},
:event_url => (Proc.new do |o|
msg = o.journaled
if msg.parent_id.nil?
{:id => msg.id}
else
{:id => msg.parent_id, :r => msg.id, :anchor => "message-#{msg.id}"}
end.reverse_merge :controller => 'messages', :action => 'show', :board_id => msg.board_id
end),
:activity_find_options => { :include => { :board => :project } }
acts_as_searchable :columns => ['subject', 'content'], acts_as_searchable :columns => ['subject', 'content'],
:include => {:board => :project}, :include => {:board => :project},
:project_key => 'project_id', :project_key => 'project_id',
:date_column => "#{table_name}.created_on" :date_column => "#{table_name}.created_on"
acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},
:description => :content,
:type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'},
:url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id}.merge(o.parent_id.nil? ? {:id => o.id} :
{:id => o.parent_id, :r => o.id, :anchor => "message-#{o.id}"})}
acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]},
:author_key => :author_id
acts_as_watchable acts_as_watchable
attr_protected :locked, :sticky attr_protected :locked, :sticky
......
...@@ -16,7 +16,10 @@ ...@@ -16,7 +16,10 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class MessageObserver < ActiveRecord::Observer class MessageObserver < ActiveRecord::Observer
def after_create(message) def after_save(message)
Mailer.deliver_message_posted(message) if Setting.notified_events.include?('message_posted') if message.last_journal.version == 1
# Only deliver mails for the first journal
Mailer.deliver_message_posted(message) if Setting.notified_events.include?('message_posted')
end
end end
end end
...@@ -24,10 +24,8 @@ class News < ActiveRecord::Base ...@@ -24,10 +24,8 @@ class News < ActiveRecord::Base
validates_length_of :title, :maximum => 60 validates_length_of :title, :maximum => 60
validates_length_of :summary, :maximum => 255 validates_length_of :summary, :maximum => 255
acts_as_journalized :event_url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.journaled_id} }
acts_as_searchable :columns => ['title', 'summary', "#{table_name}.description"], :include => :project acts_as_searchable :columns => ['title', 'summary', "#{table_name}.description"], :include => :project
acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}}
acts_as_activity_provider :find_options => {:include => [:project, :author]},
:author_key => :author_id
acts_as_watchable acts_as_watchable
after_create :add_author_as_watcher after_create :add_author_as_watcher
......
...@@ -545,8 +545,8 @@ class Query < ActiveRecord::Base ...@@ -545,8 +545,8 @@ class Query < ActiveRecord::Base
# Returns the journals # Returns the journals
# Valid options are :order, :offset, :limit # Valid options are :order, :offset, :limit
def journals(options={}) def issue_journals(options={})
Journal.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}], IssueJournal.find :all, :joins => [:user, {:issue => [:project, :author, :tracker, :status]}],
:conditions => statement, :conditions => statement,
:order => options[:order], :order => options[:order],
:limit => options[:limit], :limit => options[:limit],
......
...@@ -26,14 +26,10 @@ class TimeEntry < ActiveRecord::Base ...@@ -26,14 +26,10 @@ class TimeEntry < ActiveRecord::Base
attr_protected :project_id, :user_id, :tyear, :tmonth, :tweek attr_protected :project_id, :user_id, :tyear, :tmonth, :tweek
acts_as_customizable acts_as_customizable
acts_as_event :title => Proc.new {|o| "#{l_hours(o.hours)} (#{(o.issue || o.project).event_title})"}, acts_as_journalized :event_title => Proc.new {|o| "#{l_hours(o.hours)} (#{(o.issue || o.project).event_title})"},
:url => Proc.new {|o| {:controller => 'timelog', :action => 'index', :project_id => o.project, :issue_id => o.issue}}, :event_url => Proc.new {|o| {:controller => 'timelog', :action => 'index', :project_id => o.project, :issue_id => o.issue}},
:author => :user, :event_author => :user,
:description => :comments :event_description => :comments
acts_as_activity_provider :timestamp => "#{table_name}.created_on",
:author_key => :user_id,
:find_options => {:include => :project}
validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on
validates_numericality_of :hours, :allow_nil => true, :message => :invalid validates_numericality_of :hours, :allow_nil => true, :message => :invalid
......
...@@ -18,14 +18,22 @@ ...@@ -18,14 +18,22 @@
require 'zlib' require 'zlib'
class WikiContent < ActiveRecord::Base class WikiContent < ActiveRecord::Base
set_locking_column :version
belongs_to :page, :class_name => 'WikiPage', :foreign_key => 'page_id' belongs_to :page, :class_name => 'WikiPage', :foreign_key => 'page_id'
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
validates_presence_of :text validates_presence_of :text
validates_length_of :comments, :maximum => 255, :allow_nil => true validates_length_of :comments, :maximum => 255, :allow_nil => true
acts_as_versioned acts_as_journalized :event_type => 'wiki-page',
:event_title => Proc.new {|o| "#{l(:label_wiki_edit)}: #{o.page.title} (##{o.version})"},
:event_url => Proc.new {|o| {:controller => 'wiki', :id => o.page.wiki.project_id, :page => o.page.title, :version => o.version}},
:activity_type => 'wiki_edits',
:activity_permission => :view_wiki_edits,
:activity_find_options => { :include => { :page => { :wiki => :project } } }
def activity_type
'wiki_edits'
end
def visible?(user=User.current) def visible?(user=User.current)
page.visible?(user) page.visible?(user)
end end
...@@ -44,67 +52,71 @@ class WikiContent < ActiveRecord::Base ...@@ -44,67 +52,71 @@ class WikiContent < ActiveRecord::Base
notified.reject! {|user| !visible?(user)} notified.reject! {|user| !visible?(user)}
notified.collect(&:mail) notified.collect(&:mail)
end end
class Version
belongs_to :page, :class_name => '::WikiPage', :foreign_key => 'page_id'
belongs_to :author, :class_name => '::User', :foreign_key => 'author_id'
attr_protected :data
acts_as_event :title => Proc.new {|o| "#{l(:label_wiki_edit)}: #{o.page.title} (##{o.version})"}, # FIXME: Deprecate
:description => :comments, def versions
:datetime => :updated_on, journals
:type => 'wiki-page', end
:url => Proc.new {|o| {:controller => 'wiki', :action => 'show', :project_id => o.page.wiki.project, :id => o.page.title, :version => o.version}}
def version
unless last_journal
# FIXME: This is code that caters for a case that should never happen in the normal code paths!!
create_journal
last_journal.update_attribute(:created_at, updated_on)
end
last_journal.version
end
# FIXME: This is for backwards compatibility only. Remove once we decide it is not needed anymore
WikiContentJournal.class_eval do
attr_protected :data
after_save :compress_version_text
acts_as_activity_provider :type => 'wiki_edits', # Wiki Content might be large and the data should possibly be compressed
:timestamp => "#{WikiContent.versioned_table_name}.updated_on", def compress_version_text
:author_key => "#{WikiContent.versioned_table_name}.author_id", self.text = changes["text"].last if changes["text"]
:permission => :view_wiki_edits, self.text ||= self.journaled.text
:find_options => {:select => "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " + end
"#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title, " +
"#{WikiContent.versioned_table_name}.page_id, #{WikiContent.versioned_table_name}.author_id, " +
"#{WikiContent.versioned_table_name}.id",
:joins => "LEFT JOIN #{WikiPage.table_name} ON #{WikiPage.table_name}.id = #{WikiContent.versioned_table_name}.page_id " +
"LEFT JOIN #{Wiki.table_name} ON #{Wiki.table_name}.id = #{WikiPage.table_name}.wiki_id " +
"LEFT JOIN #{Project.table_name} ON #{Project.table_name}.id = #{Wiki.table_name}.project_id"}
def text=(plain) def text=(plain)
case Setting.wiki_compression case Setting.wiki_compression
when 'gzip' when "gzip"
begin begin
self.data = Zlib::Deflate.deflate(plain, Zlib::BEST_COMPRESSION) text_hash :text => Zlib::Deflate.deflate(plain, Zlib::BEST_COMPRESSION), :compression => Setting.wiki_compression
self.compression = 'gzip' rescue
rescue text_hash :text => plain, :compression => ''
self.data = plain end
self.compression = ''
end
else else
self.data = plain text_hash :text => plain, :compression => ''
self.compression = ''
end end
plain plain
end end
def text_hash(hash)
changes.delete("text")
changes["data"] = hash[:text]
changes["compression"] = hash[:compression]
update_attribute(:changes, changes.to_yaml)
end
def text def text
@text ||= case compression @text ||= case changes[:compression]
when 'gzip' when 'gzip'
Zlib::Inflate.inflate(data) Zlib::Inflate.inflate(data)
else else
# uncompressed data # uncompressed data
data changes["data"]
end end
end
def project
page.project
end end
# Returns the previous version or nil # Returns the previous version or nil
def previous def previous
@previous ||= WikiContent::Version.find(:first, @previous ||= journaled.journals.at(version - 1)
:order => 'version DESC', end
:include => :author,
:conditions => ["wiki_content_id = ? AND version < ?", wiki_content_id, version]) # FIXME: Deprecate
def versioned
journaled
end end
end end
end end
<% reply_links = authorize_for('issues', 'edit') -%>
<% for journal in journals %> <% for journal in journals %>
<div id="change-<%= journal.id %>" class="<%= journal.css_classes %>"> <%= render_journal issue, journal, :edit_permission => :edit_issue_notes,
<h4><div class="journal-link"><%= link_to "##{journal.indice}", :anchor => "note-#{journal.indice}" %></div> :edit_own_permission => :edit_own_issue_notes %>
<%= avatar(journal.user, :size => "24") %>
<%= content_tag('a', '', :name => "note-#{journal.indice}")%>
<%= authoring journal.created_on, journal.user, :label => :label_updated_time_by %></h4>
<% if journal.details.any? %>
<ul class="details">
<% for detail in journal.details %>
<li><%= show_detail(detail) %></li>
<% end %>
</ul>
<% end %>
<%= render_notes(issue, journal, :reply_links => reply_links) unless journal.notes.blank? %>
</div>
<%= call_hook(:view_issues_history_journal_bottom, { :journal => journal }) %> <%= call_hook(:view_issues_history_journal_bottom, { :journal => journal }) %>
<% end %> <% end %>
......
...@@ -49,9 +49,9 @@ api.issue do ...@@ -49,9 +49,9 @@ api.issue do
api.created_on journal.created_on api.created_on journal.created_on
api.array :details do api.array :details do
journal.details.each do |detail| journal.details.each do |detail|
api.detail :property => detail.property, :name => detail.prop_key do api.detail :property => journal.property(detail), :name => journal.prop_key(detail) do
api.old_value detail.old_value api.old_value journal.old_value(detail)
api.new_value detail.value api.new_value journal.value(detail)
end end
end end
end end
......
...@@ -7,7 +7,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do ...@@ -7,7 +7,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do
xml.updated((@journals.first ? @journals.first.event_datetime : Time.now).xmlschema) xml.updated((@journals.first ? @journals.first.event_datetime : Time.now).xmlschema)
xml.author { xml.name "#{Setting.app_title}" } xml.author { xml.name "#{Setting.app_title}" }
@journals.each do |change| @journals.each do |change|
issue = change.issue issue = change.journaled
xml.entry do xml.entry do
xml.title "#{issue.project.name} - #{issue.tracker.name} ##{issue.id}: #{issue.subject}" xml.title "#{issue.project.name} - #{issue.tracker.name} ##{issue.id}: #{issue.subject}"
xml.link "rel" => "alternate", "href" => url_for(:controller => 'issues' , :action => 'show', :id => issue, :only_path => false) xml.link "rel" => "alternate", "href" => url_for(:controller => 'issues' , :action => 'show', :id => issue, :only_path => false)
...@@ -20,7 +20,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do ...@@ -20,7 +20,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do
xml.content "type" => "html" do xml.content "type" => "html" do
xml.text! '<ul>' xml.text! '<ul>'
change.details.each do |detail| change.details.each do |detail|
xml.text! '<li>' + show_detail(detail, false) + '</li>' xml.text! '<li>' + change.render_detail(detail, false) + '</li>'
end end
xml.text! '</ul>' xml.text! '</ul>'
xml.text! textilizable(change, :notes, :only_path => false) unless change.notes.blank? xml.text! textilizable(change, :notes, :only_path => false) unless change.notes.blank?
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<ul> <ul>
<% for detail in @journal.details %> <% for detail in @journal.details %>
<li><%= show_detail(detail, true) %></li> <li><%= @journal.render_detail(detail, true) %></li>
<% end %> <% end %>
</ul> </ul>
......
<%= l(:text_issue_updated, :id => "##{@issue.id}", :author => @journal.user) %> <%= l(:text_issue_updated, :id => "##{@issue.id}", :author => @journal.user) %>
<% for detail in @journal.details -%> <% for detail in @journal.details -%>
<%= show_detail(detail, true) %> <%= @journal.render_detail(detail, true) %>
<% end -%> <% end -%>
<%= @journal.notes if @journal.notes? %> <%= @journal.notes if @journal.notes? %>
......
...@@ -26,7 +26,10 @@ ...@@ -26,7 +26,10 @@
<h3><%= l(:label_result_plural) %> (<%= @results_by_type.values.sum %>)</h3> <h3><%= l(:label_result_plural) %> (<%= @results_by_type.values.sum %>)</h3>
<dl id="search-results"> <dl id="search-results">
<% @results.each do |e| %> <% @results.each do |e| %>
<dt class="<%= e.event_type %>"><%= content_tag('span', h(e.project), :class => 'project') unless @project == e.project %> <%= link_to highlight_tokens(truncate(e.event_title, :length => 255), @tokens), e.event_url %></dt> <dt class="<%= e.event_type %>">
<%= content_tag('span', h(e.project), :class => 'project') unless @project == e.project %>
<%= link_to highlight_tokens(truncate(e.event_title, :length => 255), @tokens), e.event_url %>
</dt>
<dd><span class="description"><%= highlight_tokens(e.event_description, @tokens) %></span> <dd><span class="description"><%= highlight_tokens(e.event_description, @tokens) %></span>
<span class="author"><%= format_time(e.event_datetime) %></span></dd> <span class="author"><%= format_time(e.event_datetime) %></span></dd>
<% end %> <% end %>
......
<h2><%=h @page.pretty_title %></h2> <h2><%=h @page.pretty_title %></h2>
<% form_for :content, @content, :url => {:action => 'update', :id => @page.title}, :html => {:method => :put, :multipart => true, :id => 'wiki_form'} do |f| %> <% form_for :content, @content, :url => {:action => 'update', :id => @page.title}, :html => {:method => :put, :multipart => true, :id => 'wiki_form'} do |f| %>
<%= f.hidden_field :version %> <%= f.hidden_field :lock_version %>
<%= error_messages_for 'content' %> <%= error_messages_for 'content' %>
<p><%= f.text_area :text, :cols => 100, :rows => 25, :class => 'wiki-edit', :accesskey => accesskey(:edit) %></p> <p><%= f.text_area :text, :cols => 100, :rows => 25, :class => 'wiki-edit', :accesskey => accesskey(:edit) %></p>
......
...@@ -21,9 +21,9 @@ ...@@ -21,9 +21,9 @@
<td class="id"><%= link_to ver.version, :action => 'show', :id => @page.title, :project_id => @page.project, :version => ver.version %></td> <td class="id"><%= link_to ver.version, :action => 'show', :id => @page.title, :project_id => @page.project, :version => ver.version %></td>
<td class="checkbox"><%= radio_button_tag('version', ver.version, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('cbto-#{line_num+1}').checked=true;") if show_diff && (line_num < @versions.size) %></td> <td class="checkbox"><%= radio_button_tag('version', ver.version, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('cbto-#{line_num+1}').checked=true;") if show_diff && (line_num < @versions.size) %></td>
<td class="checkbox"><%= radio_button_tag('version_from', ver.version, (line_num==2), :id => "cbto-#{line_num}") if show_diff && (line_num > 1) %></td> <td class="checkbox"><%= radio_button_tag('version_from', ver.version, (line_num==2), :id => "cbto-#{line_num}") if show_diff && (line_num > 1) %></td>
<td class="updated_on"><%= format_time(ver.updated_on) %></td> <td class="updated_on"><%= format_time(ver.created_at) %></td>
<td class="author"><%= link_to_user ver.author %></td> <td class="author"><%= link_to_user ver.user %></td>
<td class="comments"><%=h ver.comments %></td> <td class="comments"><%=h ver.notes %></td>
<td class="buttons"><%= link_to l(:button_annotate), :action => 'annotate', :id => @page.title, :version => ver.version %></td> <td class="buttons"><%= link_to l(:button_annotate), :action => 'annotate', :id => @page.title, :version => ver.version %></td>
</tr> </tr>
<% line_num += 1 %> <% line_num += 1 %>
......
...@@ -36,7 +36,7 @@ Rails::Initializer.run do |config| ...@@ -36,7 +36,7 @@ Rails::Initializer.run do |config|
# Activate observers that should always be running # Activate observers that should always be running
# config.active_record.observers = :cacher, :garbage_collector # config.active_record.observers = :cacher, :garbage_collector
config.active_record.observers = :message_observer, :issue_observer, :journal_observer, :news_observer, :document_observer, :wiki_content_observer, :comment_observer config.active_record.observers = :journal_observer, :message_observer, :issue_observer, :news_observer, :document_observer, :wiki_content_observer, :comment_observer
# Make Active Record use UTC-base instead of local time # Make Active Record use UTC-base instead of local time
# config.active_record.default_timezone = :utc # config.active_record.default_timezone = :utc
......
...@@ -2,7 +2,7 @@ class RenameCommentToComments < ActiveRecord::Migration ...@@ -2,7 +2,7 @@ class RenameCommentToComments < ActiveRecord::Migration
def self.up def self.up
rename_column(:comments, :comment, :comments) if ActiveRecord::Base.connection.columns(Comment.table_name).detect{|c| c.name == "comment"} rename_column(:comments, :comment, :comments) if ActiveRecord::Base.connection.columns(Comment.table_name).detect{|c| c.name == "comment"}
rename_column(:wiki_contents, :comment, :comments) if ActiveRecord::Base.connection.columns(WikiContent.table_name).detect{|c| c.name == "comment"} rename_column(:wiki_contents, :comment, :comments) if ActiveRecord::Base.connection.columns(WikiContent.table_name).detect{|c| c.name == "comment"}
rename_column(:wiki_content_versions, :comment, :comments) if ActiveRecord::Base.connection.columns(WikiContent.versioned_table_name).detect{|c| c.name == "comment"} rename_column(:wiki_content_versions, :comment, :comments) if ActiveRecord::Base.connection.columns("wiki_content_versions").detect{|c| c.name == "comment"}
rename_column(:time_entries, :comment, :comments) if ActiveRecord::Base.connection.columns(TimeEntry.table_name).detect{|c| c.name == "comment"} rename_column(:time_entries, :comment, :comments) if ActiveRecord::Base.connection.columns(TimeEntry.table_name).detect{|c| c.name == "comment"}
rename_column(:changesets, :comment, :comments) if ActiveRecord::Base.connection.columns(Changeset.table_name).detect{|c| c.name == "comment"} rename_column(:changesets, :comment, :comments) if ActiveRecord::Base.connection.columns(Changeset.table_name).detect{|c| c.name == "comment"}
end end
......
class GeneralizeJournals < ActiveRecord::Migration
def self.up
# This is provided here for migrating up after the JournalDetails has been removed
unless Object.const_defined?("JournalDetails")
Object.const_set("JournalDetails", Class.new(ActiveRecord::Base))
end
change_table :journals do |t|
t.rename :journalized_id, :journaled_id
t.rename :created_on, :created_at
t.integer :version, :default => 0, :null => false
t.string :activity_type
t.text :changes
t.string :type
t.index :journaled_id
t.index :activity_type
t.index :created_at
t.index :type
end
Journal.all.group_by(&:journaled_id).each_pair do |id, journals|
journals.sort_by(&:created_at).each_with_index do |j, idx|
j.update_attribute(:type, "#{j.journalized_type}Journal")
j.update_attribute(:version, idx + 1)
# FIXME: Find some way to choose the right activity here
j.update_attribute(:activity_type, j.journalized_type.constantize.activity_provider_options.keys.first)
end
end
change_table :journals do |t|
t.remove :journalized_type
end
JournalDetails.all.each do |detail|
journal = Journal.find(detail.journal_id)
changes = journal.changes || {}
if detail.property == 'attr' # Standard attributes
changes[detail.prop_key.to_s] = [detail.old_value, detail.value]
elsif detail.property == 'cf' # Custom fields
changes["custom_values_" + detail.prop_key.to_s] = [detail.old_value, detail.value]
elsif detail.property == 'attachment' # Attachment
changes["attachments_" + detail.prop_key.to_s] = [detail.old_value, detail.value]
end
journal.update_attribute(:changes, changes.to_yaml)
end
# Create creation journals for all activity providers
providers = Redmine::Activity.providers.collect {|k, v| v.collect(&:constantize) }.flatten.compact.uniq
providers.each do |p|
next unless p.table_exists? # Objects not in the DB yet need creation journal entries
p.find(:all).each do |o|
unless o.last_journal
o.send(:update_journal)
created_at = nil
[:created_at, :created_on, :updated_at, :updated_on].each do |m|
if o.respond_to? m
created_at = o.send(m)
break
end
end
p "Updating #{o}"
o.last_journal.update_attribute(:created_at, created_at) if created_at and o.last_journal
end
end
end
# drop_table :journal_details
end
def self.down
# create_table "journal_details", :force => true do |t|
# t.integer "journal_id", :default => 0, :null => false
# t.string "property", :limit => 30, :default => "", :null => false
# t.string "prop_key", :limit => 30, :default => "", :null => false
# t.string "old_value"
# t.string "value"
# end
change_table "journals" do |t|
t.rename :journaled_id, :journalized_id
t.rename :created_at, :created_on
t.string :journalized_type, :limit => 30, :default => "", :null => false
end
custom_field_names = CustomField.all.group_by(&:type)[IssueCustomField].collect(&:name)
Journal.all.each do |j|
# Can't used j.journalized.class.name because the model changes make it nil
j.update_attribute(:journalized_type, j.type.to_s.sub("Journal","")) if j.type.present?
end
change_table "journals" do |t|
t.remove_index :journaled_id
t.remove_index :activity_type
t.remove_index :created_at
t.remove_index :type
t.remove :type
t.remove :version
t.remove :activity_type
t.remove :changes
end
# add_index "journal_details", ["journal_id"], :name => "journal_details_journal_id"
# add_index "journals", ["journalized_id", "journalized_type"], :name => "journals_journalized_id"
end
end
class MergeWikiVersionsWithJournals < ActiveRecord::Migration
def self.up
# This is provided here for migrating up after the WikiContent::Version class has been removed
unless WikiContent.const_defined?("Version")
WikiContent.const_set("Version", Class.new(ActiveRecord::Base))
end
WikiContent::Version.find_by_sql("SELECT * FROM wiki_content_versions").each do |wv|
journal = WikiContentJournal.create!(:journaled_id => wv.wiki_content_id, :user_id => wv.author_id,
:notes => wv.comments, :activity_type => "wiki_edits")
changes = {}
changes["compression"] = wv.compression
changes["data"] = wv.data
journal.update_attribute(:changes, changes.to_yaml)
journal.update_attribute(:version, wv.version)
end
# drop_table :wiki_content_versions
change_table :wiki_contents do |t|
t.rename :version, :lock_version
end
end
def self.down
change_table :wiki_contents do |t|
t.rename :lock_version, :version
end
# create_table :wiki_content_versions do |t|
# t.column :wiki_content_id, :integer, :null => false
# t.column :page_id, :integer, :null => false
# t.column :author_id, :integer
# t.column :data, :binary
# t.column :compression, :string, :limit => 6, :default => ""
# t.column :comments, :string, :limit => 255, :default => ""
# t.column :updated_on, :datetime, :null => false
# t.column :version, :integer, :null => false
# end
# add_index :wiki_content_versions, :wiki_content_id, :name => :wiki_content_versions_wcid
#
# WikiContentJournal.all.each do |j|
# WikiContent::Version.create(:wiki_content_id => j.journaled_id, :page_id => j.journaled.page_id,
# :author_id => j.user_id, :data => j.changes["data"], :compression => j.changes["compression"],
# :comments => j.notes, :updated_on => j.created_at, :version => j.version)
# end
WikiContentJournal.destroy_all
end
end
class Meeting < ActiveRecord::Base class Meeting < ActiveRecord::Base
belongs_to :project belongs_to :project
acts_as_event :title => Proc.new {|o| "#{o.scheduled_on} Meeting"}, acts_as_journalized :event_title => Proc.new {|o| "#{o.scheduled_on} Meeting"},
:datetime => :scheduled_on, :event_datetime => :scheduled_on,
:author => nil, :event_author => nil,
:url => Proc.new {|o| {:controller => 'meetings', :action => 'show', :id => o.id}} :event_url => Proc.new {|o| {:controller => 'meetings', :action => 'show', :id => o.id}}
:activity_timestamp => 'scheduled_on'
acts_as_activity_provider :timestamp => 'scheduled_on',
:find_options => { :include => :project }
end end
...@@ -208,12 +208,12 @@ Redmine::MenuManager.map :project_menu do |menu| ...@@ -208,12 +208,12 @@ Redmine::MenuManager.map :project_menu do |menu|
end end
Redmine::Activity.map do |activity| Redmine::Activity.map do |activity|
activity.register :issues, :class_name => %w(Issue Journal) activity.register :issues, :class_name => 'Issue'
activity.register :changesets activity.register :changesets
activity.register :news activity.register :news
activity.register :documents, :class_name => %w(Document Attachment) activity.register :documents, :class_name => %w(Document Attachment)
activity.register :files, :class_name => 'Attachment' activity.register :files, :class_name => 'Attachment'
activity.register :wiki_edits, :class_name => 'WikiContent::Version', :default => false activity.register :wiki_edits, :class_name => 'WikiContent', :default => false
activity.register :messages, :default => false activity.register :messages, :default => false
activity.register :time_entries, :default => false activity.register :time_entries, :default => false
end end
......
...@@ -38,7 +38,17 @@ module Redmine ...@@ -38,7 +38,17 @@ module Redmine
return @event_types unless @event_types.nil? return @event_types unless @event_types.nil?
@event_types = Redmine::Activity.available_event_types @event_types = Redmine::Activity.available_event_types
@event_types = @event_types.select {|o| @project.self_and_descendants.detect {|p| @user.allowed_to?("view_#{o}".to_sym, p)}} if @project if @project
@event_types = @event_types.select do |o|
@project.self_and_descendants.detect do |p|
permissions = constantized_providers(o).collect do |p|
p.activity_provider_options[o].try(:[], :permission)
end.compact
return @user.allowed_to?("view_#{o}".to_sym, @project) if permissions.blank?
permissions.all? {|p| @user.allowed_to?(p, @project) } if @project
end
end
end
@event_types @event_types
end end
......
...@@ -359,13 +359,13 @@ module Redmine ...@@ -359,13 +359,13 @@ module Redmine
pdf.SetFontStyle('B',9) pdf.SetFontStyle('B',9)
pdf.RDMCell(190,5, l(:label_history), "B") pdf.RDMCell(190,5, l(:label_history), "B")
pdf.Ln pdf.Ln
for journal in issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC") for journal in issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_at ASC")
pdf.SetFontStyle('B',8) pdf.SetFontStyle('B',8)
pdf.RDMCell(190,5, format_time(journal.created_on) + " - " + journal.user.name) pdf.RDMCell(190,5, format_time(journal.created_at) + " - " + journal.user.name)
pdf.Ln pdf.Ln
pdf.SetFontStyle('I',8) pdf.SetFontStyle('I',8)
for detail in journal.details for detail in journal.details
pdf.RDMCell(190,5, "- " + show_detail(detail, true)) pdf.RDMCell(190,5, "- " + journal.render_detail(detail, true))
pdf.Ln pdf.Ln
end end
if journal.notes? if journal.notes?
......
...@@ -95,7 +95,7 @@ module Redmine ...@@ -95,7 +95,7 @@ module Redmine
page = nil page = nil
if args.size > 0 if args.size > 0
page = Wiki.find_page(args.first.to_s, :project => @project) page = Wiki.find_page(args.first.to_s, :project => @project)
elsif obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version) elsif obj.is_a?(WikiContent)
page = obj.page page = obj.page
else else
raise 'With no argument, this macro can be called from wiki pages only.' raise 'With no argument, this macro can be called from wiki pages only.'
......
---
journal_details_001:
old_value: "1"
property: attr
id: 1
value: "2"
prop_key: status_id
journal_id: 1
journal_details_002:
old_value: "40"
property: attr
id: 2
value: "30"
prop_key: done_ratio
journal_id: 1
journal_details_003:
old_value: nil
property: attr
id: 3
value: "6"
prop_key: fixed_version_id
journal_id: 4
journal_details_004:
old_value: "This word was removed and an other was"
property: attr
id: 4
value: "This word was and an other was added"
prop_key: description
journal_id: 3
journal_details_005:
old_value: Old value
property: cf
id: 5
value: New value
prop_key: 2
journal_id: 3
--- ---
journals_001: journals_001:
created_on: <%= 2.days.ago.to_date.to_s(:db) %>
notes: "Journal notes"
id: 1 id: 1
journalized_type: Issue type: "IssueJournal"
activity_type: "issues"
created_at: <%= 3.days.ago.to_date.to_s(:db) %>
version: 1
user_id: 1 user_id: 1
journalized_id: 1 notes: "Journal notes"
journals_002: journaled_id: 1
created_on: <%= 1.days.ago.to_date.to_s(:db) %> changes: |
notes: "Some notes with Redmine links: #2, r2." ---
status_id:
- 1
- 2
done_ratio:
- 40
- 30
journals_002:
id: 2 id: 2
journalized_type: Issue type: "IssueJournal"
activity_type: "issues"
created_at: <%= 1.days.ago.to_date.to_s(:db) %>
version: 2
user_id: 2 user_id: 2
journalized_id: 1 notes: "Some notes with Redmine links: #2, r2."
journals_003: journaled_id: 1
created_on: <%= 1.days.ago.to_date.to_s(:db) %> changes: "--- {}"
notes: "A comment with inline image: !picture.jpg!" journals_003:
id: 3 id: 3
journalized_type: Issue type: "IssueJournal"
activity_type: "issues"
created_at: <%= 2.days.ago.to_date.to_s(:db) %>
version: 1
user_id: 2 user_id: 2
journalized_id: 2 notes: "A comment with inline image: !picture.jpg!"
journals_004: journaled_id: 2
created_on: <%= 1.days.ago.to_date.to_s(:db) %> changes: "--- {}"
notes: "A comment with a private version." journals_004:
id: 4 id: 4
journalized_type: Issue type: "IssueJournal"
activity_type: "issues"
created_at: <%= 1.days.ago.to_date.to_s(:db) %>
version: 1
user_id: 2
notes: "A comment with a private version."
journaled_id: 6
changes: |
---
fixed_version_id:
-
- 6
journals_005:
id: 5
type: "IssueJournal"
activity_type: "issues"
created_at: <%= 2.days.ago.to_date.to_s(:db) %>
version: 0
user_id: 1
notes:
journaled_id: 5
changes: "--- {}"
journals_006:
id: 6
type: "WikiContentJournal"
activity_type: "wiki_edits"
created_at: 2007-03-07 00:08:07 +01:00
version: 1
user_id: 2
notes: Page creation
journaled_id: 1
changes: |
---
compression: ""
data: |-
h1. CookBook documentation
Some [[documentation]] here...
journals_007:
id: 7
type: "WikiContentJournal"
activity_type: "wiki_edits"
created_at: 2007-03-07 00:08:34 +01:00
version: 2
user_id: 1 user_id: 1
journalized_id: 6 notes: Small update
journaled_id: 1
changes: |
---
compression: ""
data: |-
h1. CookBook documentation
Some updated [[documentation]] here...
journals_008:
id: 8
type: "WikiContentJournal"
activity_type: "wiki_edits"
created_at: 2007-03-07 00:10:51 +01:00
version: 3
user_id: 1
notes: ""
journaled_id: 1
changes: |
---
compression: ""
data: |-
h1. CookBook documentation
Some updated [[documentation]] here...
journals_009:
id: 9
type: "WikiContentJournal"
activity_type: "wiki_edits"
created_at: 2007-03-08 00:18:07 +01:00
version: 1
user_id: 1
notes:
journaled_id: 2
changes: |
---
data: |-
h1. Another page
This is a link to a ticket: #2
journals_010:
id: 10
type: "MessageJournal"
activity_type: "messages"
created_at: <%= 2.days.ago.to_date.to_s(:db) %>
version: 1
user_id: 1
notes:
journaled_id: 5
changes: --- {}
journals_011:
id: 11
type: "AttachmentJournal"
activity_type: "files"
created_at: <%= 2.days.ago.to_date.to_s(:db) %>
version: 1
user_id: 2
notes: "An attachment on a version"
journaled_id: 9
changes: --- {}
journals_012:
id: 12
type: "AttachmentJournal"
activity_type: "files"
created_at: <%= 2.days.ago.to_date.to_s(:db) %>
version: 1
user_id: 2
notes: "An attachment on a project"
journaled_id: 8
changes: --- {}
journals_013:
id: 13
type: "AttachmentJournal"
activity_type: "files"
created_at: <%= 2.days.ago.to_date.to_s(:db) %>
version: 1
user_id: 2
notes: "An attachment on an issue"
journaled_id: 7
changes: --- {}
journals_014:
id: 14
type: "AttachmentJournal"
activity_type: "documents"
created_at: <%= 2.days.ago.to_date.to_s(:db) %>
version: 1
user_id: 2
notes: "An attachment on a document"
journaled_id: 2
changes: --- {}
journals_015:
id: 15
type: "MessageJournal"
activity_type: "messages"
created_at: <%= 2.days.ago.to_date.to_s(:db) %>
version: 1
user_id: 2
notes: "A message journal"
journaled_id: 1
journals_016:
id: 16
type: "MessageJournal"
activity_type: "messages"
created_at: <%= 2.days.ago.to_date.to_s(:db) %>
version: 1
user_id: 2
notes: "A message journal"
journaled_id: 2
journals_017:
id: 17
type: "MessageJournal"
activity_type: "messages"
created_at: <%= 2.days.ago.to_date.to_s(:db) %>
version: 1
user_id: 2
notes: "A message journal"
journaled_id: 3
journals_018:
id: 18
type: "IssueJournal"
activity_type: "issues"
created_at: <%= 3.days.ago.to_date.to_s(:db) %>
version: 0
user_id: 2
notes:
journaled_id: 11
changes: "--- {}"
\ No newline at end of file
---
wiki_content_versions_001:
updated_on: 2007-03-07 00:08:07 +01:00
page_id: 1
id: 1
version: 1
author_id: 2
comments: Page creation
wiki_content_id: 1
compression: ""
data: |-
h1. CookBook documentation
Some [[documentation]] here...
wiki_content_versions_002:
updated_on: 2007-03-07 00:08:34 +01:00
page_id: 1
id: 2
version: 2
author_id: 1
comments: Small update
wiki_content_id: 1
compression: ""
data: |-
h1. CookBook documentation
Some updated [[documentation]] here...
wiki_content_versions_003:
updated_on: 2007-03-07 00:10:51 +01:00
page_id: 1
id: 3
version: 3
author_id: 1
comments: ""
wiki_content_id: 1
compression: ""
data: |-
h1. CookBook documentation
Some updated [[documentation]] here...
wiki_content_versions_004:
data: |-
h1. Another page
This is a link to a ticket: #2
updated_on: 2007-03-08 00:18:07 +01:00
page_id: 2
wiki_content_id: 2
id: 4
version: 1
author_id: 1
comments:
...@@ -9,7 +9,7 @@ wiki_contents_001: ...@@ -9,7 +9,7 @@ wiki_contents_001:
updated_on: 2007-03-07 00:10:51 +01:00 updated_on: 2007-03-07 00:10:51 +01:00
page_id: 1 page_id: 1
id: 1 id: 1
version: 3 lock_version: 3
author_id: 1 author_id: 1
comments: Gzip compression activated comments: Gzip compression activated
wiki_contents_002: wiki_contents_002:
...@@ -22,7 +22,7 @@ wiki_contents_002: ...@@ -22,7 +22,7 @@ wiki_contents_002:
updated_on: 2007-03-08 00:18:07 +01:00 updated_on: 2007-03-08 00:18:07 +01:00
page_id: 2 page_id: 2
id: 2 id: 2
version: 1 lock_version: 1
author_id: 1 author_id: 1
comments: comments:
wiki_contents_003: wiki_contents_003:
...@@ -33,7 +33,7 @@ wiki_contents_003: ...@@ -33,7 +33,7 @@ wiki_contents_003:
updated_on: 2007-03-08 00:18:07 +01:00 updated_on: 2007-03-08 00:18:07 +01:00
page_id: 3 page_id: 3
id: 3 id: 3
version: 1 lock_version: 1
author_id: 1 author_id: 1
comments: comments:
wiki_contents_004: wiki_contents_004:
...@@ -46,7 +46,7 @@ wiki_contents_004: ...@@ -46,7 +46,7 @@ wiki_contents_004:
updated_on: 2007-03-08 00:18:07 +01:00 updated_on: 2007-03-08 00:18:07 +01:00
page_id: 4 page_id: 4
id: 4 id: 4
version: 1 lock_version: 1
author_id: 1 author_id: 1
comments: comments:
wiki_contents_005: wiki_contents_005:
...@@ -57,7 +57,7 @@ wiki_contents_005: ...@@ -57,7 +57,7 @@ wiki_contents_005:
updated_on: 2007-03-08 00:18:07 +01:00 updated_on: 2007-03-08 00:18:07 +01:00
page_id: 5 page_id: 5
id: 5 id: 5
version: 1 lock_version: 1
author_id: 1 author_id: 1
comments: comments:
wiki_contents_006: wiki_contents_006:
...@@ -68,7 +68,7 @@ wiki_contents_006: ...@@ -68,7 +68,7 @@ wiki_contents_006:
updated_on: 2007-03-08 00:18:07 +01:00 updated_on: 2007-03-08 00:18:07 +01:00
page_id: 6 page_id: 6
id: 6 id: 6
version: 1 lock_version: 1
author_id: 1 author_id: 1
comments: comments:
wiki_contents_007: wiki_contents_007:
...@@ -76,7 +76,7 @@ wiki_contents_007: ...@@ -76,7 +76,7 @@ wiki_contents_007:
updated_on: 2007-03-08 00:18:07 +01:00 updated_on: 2007-03-08 00:18:07 +01:00
page_id: 7 page_id: 7
id: 7 id: 7
version: 1 lock_version: 1
author_id: 1 author_id: 1
comments: comments:
wiki_contents_008: wiki_contents_008:
...@@ -84,7 +84,7 @@ wiki_contents_008: ...@@ -84,7 +84,7 @@ wiki_contents_008:
updated_on: 2007-03-08 00:18:07 +01:00 updated_on: 2007-03-08 00:18:07 +01:00
page_id: 8 page_id: 8
id: 8 id: 8
version: 1 lock_version: 1
author_id: 1 author_id: 1
comments: comments:
wiki_contents_009: wiki_contents_009:
...@@ -92,7 +92,7 @@ wiki_contents_009: ...@@ -92,7 +92,7 @@ wiki_contents_009:
updated_on: 2007-03-08 00:18:07 +01:00 updated_on: 2007-03-08 00:18:07 +01:00
page_id: 9 page_id: 9
id: 9 id: 9
version: 1 lock_version: 1
author_id: 1 author_id: 1
comments: comments:
wiki_contents_010: wiki_contents_010:
...@@ -100,7 +100,7 @@ wiki_contents_010: ...@@ -100,7 +100,7 @@ wiki_contents_010:
updated_on: 2007-03-08 00:18:07 +01:00 updated_on: 2007-03-08 00:18:07 +01:00
page_id: 10 page_id: 10
id: 10 id: 10
version: 1 lock_version: 1
author_id: 1 author_id: 1
comments: comments:
\ No newline at end of file
...@@ -10,10 +10,10 @@ class ActivitiesControllerTest < ActionController::TestCase ...@@ -10,10 +10,10 @@ class ActivitiesControllerTest < ActionController::TestCase
assert_not_nil assigns(:events_by_day) assert_not_nil assigns(:events_by_day)
assert_tag :tag => "h3", assert_tag :tag => "h3",
:content => /#{2.days.ago.to_date.day}/, :content => /#{1.day.ago.to_date.day}/,
:sibling => { :tag => "dl", :sibling => { :tag => "dl",
:child => { :tag => "dt", :child => { :tag => "dt",
:attributes => { :class => /issue-edit/ }, :attributes => { :class => /issue/ },
:child => { :tag => "a", :child => { :tag => "a",
:content => /(#{IssueStatus.find(2).name})/, :content => /(#{IssueStatus.find(2).name})/,
} }
...@@ -46,12 +46,12 @@ class ActivitiesControllerTest < ActionController::TestCase ...@@ -46,12 +46,12 @@ class ActivitiesControllerTest < ActionController::TestCase
assert_not_nil assigns(:events_by_day) assert_not_nil assigns(:events_by_day)
assert_tag :tag => "h3", assert_tag :tag => "h3",
:content => /#{5.day.ago.to_date.day}/, :content => /#{3.day.ago.to_date.day}/,
:sibling => { :tag => "dl", :sibling => { :tag => "dl",
:child => { :tag => "dt", :child => { :tag => "dt",
:attributes => { :class => /issue/ }, :attributes => { :class => /issue/ },
:child => { :tag => "a", :child => { :tag => "a",
:content => /#{Issue.find(5).subject}/, :content => /#{Issue.find(1).subject}/,
} }
} }
} }
......
...@@ -120,10 +120,9 @@ class AttachmentsControllerTest < ActionController::TestCase ...@@ -120,10 +120,9 @@ class AttachmentsControllerTest < ActionController::TestCase
# no referrer # no referrer
assert_redirected_to '/projects/ecookbook' assert_redirected_to '/projects/ecookbook'
assert_nil Attachment.find_by_id(1) assert_nil Attachment.find_by_id(1)
j = issue.journals.find(:first, :order => 'created_on DESC') j = issue.journals.find(:first, :order => 'created_at DESC')
assert_equal 'attachment', j.details.first.property assert_equal ['attachments_1'], j.details.keys
assert_equal '1', j.details.first.prop_key assert_equal 'error281.txt', j.details['attachments_1'].first
assert_equal 'error281.txt', j.details.first.old_value
end end
def test_destroy_wiki_page_attachment def test_destroy_wiki_page_attachment
......
This diff is collapsed.
...@@ -43,7 +43,6 @@ class IssuesControllerTransactionTest < ActionController::TestCase ...@@ -43,7 +43,6 @@ class IssuesControllerTransactionTest < ActionController::TestCase
:custom_fields_trackers, :custom_fields_trackers,
:time_entries, :time_entries,
:journals, :journals,
:journal_details,
:queries :queries
self.use_transactional_fixtures = false self.use_transactional_fixtures = false
......
...@@ -22,9 +22,8 @@ require 'journals_controller' ...@@ -22,9 +22,8 @@ require 'journals_controller'
class JournalsController; def rescue_action(e) raise e end; end class JournalsController; def rescue_action(e) raise e end; end
class JournalsControllerTest < ActionController::TestCase class JournalsControllerTest < ActionController::TestCase
fixtures :projects, :users, :members, :member_roles, :roles, :issues, :journals, :journal_details, :enabled_modules, fixtures :projects, :users, :members, :member_roles, :roles, :issues, :journals, :enabled_modules
:trackers, :issue_statuses, :enumerations, :custom_fields, :custom_values, :custom_fields_projects
def setup def setup
@controller = JournalsController.new @controller = JournalsController.new
@request = ActionController::TestRequest.new @request = ActionController::TestRequest.new
...@@ -32,46 +31,6 @@ class JournalsControllerTest < ActionController::TestCase ...@@ -32,46 +31,6 @@ class JournalsControllerTest < ActionController::TestCase
User.current = nil User.current = nil
end end
def test_index
get :index, :project_id => 1
assert_response :success
assert_not_nil assigns(:journals)
assert_equal 'application/atom+xml', @response.content_type
end
def test_diff
get :diff, :id => 3, :detail_id => 4
assert_response :success
assert_template 'diff'
assert_tag 'span',
:attributes => {:class => 'diff_out'},
:content => /removed/
assert_tag 'span',
:attributes => {:class => 'diff_in'},
:content => /added/
end
def test_reply_to_issue
@request.session[:user_id] = 2
get :new, :id => 6
assert_response :success
assert_select_rjs :show, "update"
end
def test_reply_to_issue_without_permission
@request.session[:user_id] = 7
get :new, :id => 6
assert_response 403
end
def test_reply_to_note
@request.session[:user_id] = 2
get :new, :id => 6, :journal_id => 4
assert_response :success
assert_select_rjs :show, "update"
end
def test_get_edit def test_get_edit
@request.session[:user_id] = 1 @request.session[:user_id] = 1
xhr :get, :edit, :id => 2 xhr :get, :edit, :id => 2
......
...@@ -22,7 +22,7 @@ require 'projects_controller' ...@@ -22,7 +22,7 @@ require 'projects_controller'
class ProjectsController; def rescue_action(e) raise e end; end class ProjectsController; def rescue_action(e) raise e end; end
class ProjectsControllerTest < ActionController::TestCase class ProjectsControllerTest < ActionController::TestCase
fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details, fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals,
:trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages, :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
:attachments, :custom_fields, :custom_values, :time_entries :attachments, :custom_fields, :custom_values, :time_entries
......
...@@ -38,7 +38,7 @@ class SearchControllerTest < ActionController::TestCase ...@@ -38,7 +38,7 @@ class SearchControllerTest < ActionController::TestCase
assert assigns(:results).include?(Changeset.find(101)) assert assigns(:results).include?(Changeset.find(101))
assert_tag :dt, :attributes => { :class => /issue/ }, assert_tag :dt, :attributes => { :class => /issue/ },
:child => { :tag => 'a', :content => /Add ingredients categories/ }, :child => { :tag => 'a', :content => /Add ingredients categories/ },
:sibling => { :tag => 'dd', :content => /should be classified by categories/ } :sibling => { :tag => 'dd', :content => /A comment with inline image: !picture.jpg!/ }
assert assigns(:results_by_type).is_a?(Hash) assert assigns(:results_by_type).is_a?(Hash)
assert_equal 5, assigns(:results_by_type)['changesets'] assert_equal 5, assigns(:results_by_type)['changesets']
......
...@@ -22,8 +22,8 @@ require 'wiki_controller' ...@@ -22,8 +22,8 @@ require 'wiki_controller'
class WikiController; def rescue_action(e) raise e end; end class WikiController; def rescue_action(e) raise e end; end
class WikiControllerTest < ActionController::TestCase class WikiControllerTest < ActionController::TestCase
fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions, :attachments fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :wikis, :wiki_pages, :wiki_contents, :journals, :attachments
def setup def setup
@controller = WikiController.new @controller = WikiController.new
@request = ActionController::TestRequest.new @request = ActionController::TestRequest.new
...@@ -83,8 +83,7 @@ class WikiControllerTest < ActionController::TestCase ...@@ -83,8 +83,7 @@ class WikiControllerTest < ActionController::TestCase
put :update, :project_id => 1, put :update, :project_id => 1,
:id => 'New page', :id => 'New page',
:content => {:comments => 'Created the page', :content => {:comments => 'Created the page',
:text => "h1. New page\n\nThis is a new page", :text => "h1. New page\n\nThis is a new page" }
:version => 0}
assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'New_page' assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'New_page'
page = Project.find(1).wiki.find_page('New page') page = Project.find(1).wiki.find_page('New page')
assert !page.new_record? assert !page.new_record?
...@@ -100,7 +99,7 @@ class WikiControllerTest < ActionController::TestCase ...@@ -100,7 +99,7 @@ class WikiControllerTest < ActionController::TestCase
:id => 'New page', :id => 'New page',
:content => {:comments => 'Created the page', :content => {:comments => 'Created the page',
:text => "h1. New page\n\nThis is a new page", :text => "h1. New page\n\nThis is a new page",
:version => 0}, :lock_version => 0},
:attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}} :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
end end
end end
...@@ -113,13 +112,13 @@ class WikiControllerTest < ActionController::TestCase ...@@ -113,13 +112,13 @@ class WikiControllerTest < ActionController::TestCase
@request.session[:user_id] = 2 @request.session[:user_id] = 2
assert_no_difference 'WikiPage.count' do assert_no_difference 'WikiPage.count' do
assert_no_difference 'WikiContent.count' do assert_no_difference 'WikiContent.count' do
assert_difference 'WikiContent::Version.count' do assert_difference 'WikiContentJournal.count' do
put :update, :project_id => 1, put :update, :project_id => 1,
:id => 'Another_page', :id => 'Another_page',
:content => { :content => {
:comments => "my comments", :comments => "my comments",
:text => "edited", :text => "edited",
:version => 1 :lock_version => 1
} }
end end
end end
...@@ -136,13 +135,13 @@ class WikiControllerTest < ActionController::TestCase ...@@ -136,13 +135,13 @@ class WikiControllerTest < ActionController::TestCase
@request.session[:user_id] = 2 @request.session[:user_id] = 2
assert_no_difference 'WikiPage.count' do assert_no_difference 'WikiPage.count' do
assert_no_difference 'WikiContent.count' do assert_no_difference 'WikiContent.count' do
assert_no_difference 'WikiContent::Version.count' do assert_no_difference 'WikiContentJournal.count' do
put :update, :project_id => 1, put :update, :project_id => 1,
:id => 'Another_page', :id => 'Another_page',
:content => { :content => {
:comments => 'a' * 300, # failure here, comment is too long :comments => 'a' * 300, # failure here, comment is too long
:text => 'edited', :text => 'edited',
:version => 1 :lock_version => 1
} }
end end
end end
...@@ -152,7 +151,7 @@ class WikiControllerTest < ActionController::TestCase ...@@ -152,7 +151,7 @@ class WikiControllerTest < ActionController::TestCase
assert_error_tag :descendant => {:content => /Comment is too long/} assert_error_tag :descendant => {:content => /Comment is too long/}
assert_tag :tag => 'textarea', :attributes => {:id => 'content_text'}, :content => 'edited' assert_tag :tag => 'textarea', :attributes => {:id => 'content_text'}, :content => 'edited'
assert_tag :tag => 'input', :attributes => {:id => 'content_version', :value => '1'} assert_tag :tag => 'input', :attributes => {:id => 'content_lock_version', :value => '1'}
end end
def test_update_stale_page_should_not_raise_an_error def test_update_stale_page_should_not_raise_an_error
...@@ -164,13 +163,13 @@ class WikiControllerTest < ActionController::TestCase ...@@ -164,13 +163,13 @@ class WikiControllerTest < ActionController::TestCase
assert_no_difference 'WikiPage.count' do assert_no_difference 'WikiPage.count' do
assert_no_difference 'WikiContent.count' do assert_no_difference 'WikiContent.count' do
assert_no_difference 'WikiContent::Version.count' do assert_no_difference 'WikiContentJournal.count' do
put :update, :project_id => 1, put :update, :project_id => 1,
:id => 'Another_page', :id => 'Another_page',
:content => { :content => {
:comments => 'My comments', :comments => 'My comments',
:text => 'Text should not be lost', :text => 'Text should not be lost',
:version => 1 :lock_version => 1
} }
end end
end end
...@@ -196,7 +195,7 @@ class WikiControllerTest < ActionController::TestCase ...@@ -196,7 +195,7 @@ class WikiControllerTest < ActionController::TestCase
xhr :post, :preview, :project_id => 1, :id => 'CookBook_documentation', xhr :post, :preview, :project_id => 1, :id => 'CookBook_documentation',
:content => { :comments => '', :content => { :comments => '',
:text => 'this is a *previewed text*', :text => 'this is a *previewed text*',
:version => 3 } :lock_version => 3 }
assert_response :success assert_response :success
assert_template 'common/_preview' assert_template 'common/_preview'
assert_tag :tag => 'strong', :content => /previewed text/ assert_tag :tag => 'strong', :content => /previewed text/
...@@ -207,7 +206,7 @@ class WikiControllerTest < ActionController::TestCase ...@@ -207,7 +206,7 @@ class WikiControllerTest < ActionController::TestCase
xhr :post, :preview, :project_id => 1, :id => 'New page', xhr :post, :preview, :project_id => 1, :id => 'New page',
:content => { :text => 'h1. New page', :content => { :text => 'h1. New page',
:comments => '', :comments => '',
:version => 0 } :lock_version => 0 }
assert_response :success assert_response :success
assert_template 'common/_preview' assert_template 'common/_preview'
assert_tag :tag => 'h1', :content => /New page/ assert_tag :tag => 'h1', :content => /New page/
......
...@@ -39,7 +39,6 @@ class ApiTest::IssuesTest < ActionController::IntegrationTest ...@@ -39,7 +39,6 @@ class ApiTest::IssuesTest < ActionController::IntegrationTest
:custom_fields_trackers, :custom_fields_trackers,
:time_entries, :time_entries,
:journals, :journals,
:journal_details,
:queries :queries
def setup def setup
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
require File.expand_path('../../../test_helper', __FILE__) require File.expand_path('../../../test_helper', __FILE__)
class ApiTest::ProjectsTest < ActionController::IntegrationTest class ApiTest::ProjectsTest < ActionController::IntegrationTest
fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details, fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals,
:trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages, :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
:attachments, :custom_fields, :custom_values, :time_entries :attachments, :custom_fields, :custom_values, :time_entries
......
...@@ -166,21 +166,17 @@ class ActiveSupport::TestCase ...@@ -166,21 +166,17 @@ class ActiveSupport::TestCase
end end
should "use the new value's name" do should "use the new value's name" do
@detail = JournalDetail.generate!(:property => 'attr', @detail = IssueJournal.generate(:version => 1, :journaled => Issue.last)
:old_value => @old_value.id, @detail.update_attribute(:changes, {prop_key => [@old_value.id, @new_value.id]}.to_yaml)
:value => @new_value.id,
:prop_key => prop_key) assert_match @new_value.class.find(@new_value.id).name, @detail.render_detail(prop_key, true)
assert_match @new_value.name, show_detail(@detail, true)
end end
should "use the old value's name" do should "use the old value's name" do
@detail = JournalDetail.generate!(:property => 'attr', @detail = IssueJournal.generate(:version => 1, :journaled => Issue.last)
:old_value => @old_value.id, @detail.update_attribute(:changes, {prop_key => [@old_value.id, @new_value.id]}.to_yaml)
:value => @new_value.id,
:prop_key => prop_key) assert_match @old_value.class.find(@old_value.id).name, @detail.render_detail(prop_key, true)
assert_match @old_value.name, show_detail(@detail, true)
end end
end end
end end
......
...@@ -18,11 +18,16 @@ ...@@ -18,11 +18,16 @@
require File.expand_path('../../test_helper', __FILE__) require File.expand_path('../../test_helper', __FILE__)
class ActivityTest < ActiveSupport::TestCase class ActivityTest < ActiveSupport::TestCase
fixtures :projects, :versions, :attachments, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details, fixtures :projects, :versions, :attachments, :users, :roles, :members, :member_roles, :issues, :journals,
:trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages
def setup def setup
@project = Project.find(1) @project = Project.find(1)
[1,4,5,6].each do |issue_id|
i = Issue.find(issue_id)
i.init_journal(User.current, "A journal to find")
i.save!
end
end end
def test_activity_without_subprojects def test_activity_without_subprojects
...@@ -51,7 +56,7 @@ class ActivityTest < ActiveSupport::TestCase ...@@ -51,7 +56,7 @@ class ActivityTest < ActiveSupport::TestCase
assert events.include?(Issue.find(1)) assert events.include?(Issue.find(1))
assert events.include?(Message.find(5)) assert events.include?(Message.find(5))
# Issue of a private project # Issue of a private project
assert !events.include?(Issue.find(4)) assert !events.include?(Issue.find(6))
end end
def test_global_activity_logged_user def test_global_activity_logged_user
...@@ -60,7 +65,7 @@ class ActivityTest < ActiveSupport::TestCase ...@@ -60,7 +65,7 @@ class ActivityTest < ActiveSupport::TestCase
assert events.include?(Issue.find(1)) assert events.include?(Issue.find(1))
# Issue of a private project the user belongs to # Issue of a private project the user belongs to
assert events.include?(Issue.find(4)) assert events.include?(Issue.find(6))
end end
def test_user_activity def test_user_activity
...@@ -78,15 +83,18 @@ class ActivityTest < ActiveSupport::TestCase ...@@ -78,15 +83,18 @@ class ActivityTest < ActiveSupport::TestCase
events = f.events events = f.events
assert_kind_of Array, events assert_kind_of Array, events
assert events.include?(Attachment.find_by_container_type_and_container_id('Project', 1)) assert events.include?(Attachment.find_by_container_type_and_container_id('Project', 1).last_journal)
assert events.include?(Attachment.find_by_container_type_and_container_id('Version', 1)) assert events.include?(Attachment.find_by_container_type_and_container_id('Version', 1).last_journal)
assert_equal [Attachment], events.collect(&:class).uniq assert_equal [Attachment], events.collect(&:journaled).collect(&:class).uniq
assert_equal %w(Project Version), events.collect(&:container_type).uniq.sort assert_equal %w(Project Version), events.collect(&:journaled).collect(&:container_type).uniq.sort
end end
private private
def find_events(user, options={}) def find_events(user, options={})
Redmine::Activity::Fetcher.new(user, options).events(Date.today - 30, Date.today + 1) events = Redmine::Activity::Fetcher.new(user, options).events(Date.today - 30, Date.today + 1)
# Because events are provided by the journals, but we want to test for
# their targets here, transform that
events.group_by(&:journaled).keys
end end
end end
...@@ -43,29 +43,34 @@ class IssuesHelperTest < HelperTestCase ...@@ -43,29 +43,34 @@ class IssuesHelperTest < HelperTestCase
def request def request
@request ||= ActionController::TestRequest.new @request ||= ActionController::TestRequest.new
end end
# This is probably needed in this test only anymore
def show_detail(journal, detail, html = true)
journal.render_detail(detail, html)
end
context "IssuesHelper#show_detail" do context "IssuesHelper#show_detail" do
context "with no_html" do context "with no_html" do
should 'show a changing attribute' do should 'show a changing attribute' do
@detail = JournalDetail.generate!(:property => 'attr', :old_value => '40', :value => '100', :prop_key => 'done_ratio') @journal = IssueJournal.generate!(:changes => {"done_ratio" => [40, 100]}, :journaled => Issue.last)
assert_equal "% Done changed from 40 to 100", show_detail(@detail, true) assert_equal "% Done changed from 40 to 100", show_detail(@journal, @journal.details.to_a.first, true)
end end
should 'show a new attribute' do should 'show a new attribute' do
@detail = JournalDetail.generate!(:property => 'attr', :old_value => nil, :value => '100', :prop_key => 'done_ratio') @journal = IssueJournal.generate!(:changes => {"done_ratio" => [nil, 100]}, :journaled => Issue.last)
assert_equal "% Done set to 100", show_detail(@detail, true) assert_equal "% Done set to 100", show_detail(@journal, @journal.details.to_a.first, true)
end end
should 'show a deleted attribute' do should 'show a deleted attribute' do
@detail = JournalDetail.generate!(:property => 'attr', :old_value => '50', :value => nil, :prop_key => 'done_ratio') @journal = IssueJournal.generate!(:changes => {"done_ratio" => [50, nil]}, :journaled => Issue.last)
assert_equal "% Done deleted (50)", show_detail(@detail, true) assert_equal "% Done deleted (50)", show_detail(@journal, @journal.details.to_a.first, true)
end end
end end
context "with html" do context "with html" do
should 'show a changing attribute with HTML highlights' do should 'show a changing attribute with HTML highlights' do
@detail = JournalDetail.generate!(:property => 'attr', :old_value => '40', :value => '100', :prop_key => 'done_ratio') @journal = IssueJournal.generate!(:changes => {"done_ratio" => [40, 100]}, :journaled => Issue.last)
@response.body = show_detail(@detail, false) @response.body = show_detail(@journal, @journal.details.to_a.first, false)
assert_select 'strong', :text => '% Done' assert_select 'strong', :text => '% Done'
assert_select 'i', :text => '40' assert_select 'i', :text => '40'
...@@ -73,16 +78,16 @@ class IssuesHelperTest < HelperTestCase ...@@ -73,16 +78,16 @@ class IssuesHelperTest < HelperTestCase
end end
should 'show a new attribute with HTML highlights' do should 'show a new attribute with HTML highlights' do
@detail = JournalDetail.generate!(:property => 'attr', :old_value => nil, :value => '100', :prop_key => 'done_ratio') @journal = IssueJournal.generate!(:changes => {"done_ratio" => [nil, 100]}, :journaled => Issue.last)
@response.body = show_detail(@detail, false) @response.body = show_detail(@journal, @journal.details.to_a.first, false)
assert_select 'strong', :text => '% Done' assert_select 'strong', :text => '% Done'
assert_select 'i', :text => '100' assert_select 'i', :text => '100'
end end
should 'show a deleted attribute with HTML highlights' do should 'show a deleted attribute with HTML highlights' do
@detail = JournalDetail.generate!(:property => 'attr', :old_value => '50', :value => nil, :prop_key => 'done_ratio') @journal = IssueJournal.generate!(:changes => {"done_ratio" => [50, nil]}, :journaled => Issue.last)
@response.body = show_detail(@detail, false) @response.body = show_detail(@journal, @journal.details.to_a.first, false)
assert_select 'strong', :text => '% Done' assert_select 'strong', :text => '% Done'
assert_select 'strike' do assert_select 'strike' do
...@@ -93,25 +98,25 @@ class IssuesHelperTest < HelperTestCase ...@@ -93,25 +98,25 @@ class IssuesHelperTest < HelperTestCase
context "with a start_date attribute" do context "with a start_date attribute" do
should "format the current date" do should "format the current date" do
@detail = JournalDetail.generate!(:property => 'attr', :old_value => '2010-01-01', :value => '2010-01-31', :prop_key => 'start_date') @journal = IssueJournal.generate!(:changes => {"start_date" => ['2010-01-01', '2010-01-31']}, :journaled => Issue.last)
assert_match "01/31/2010", show_detail(@detail, true) assert_match "01/31/2010", show_detail(@journal, @journal.details.to_a.first, true)
end end
should "format the old date" do should "format the old date" do
@detail = JournalDetail.generate!(:property => 'attr', :old_value => '2010-01-01', :value => '2010-01-31', :prop_key => 'start_date') @journal = IssueJournal.generate!(:changes => {"start_date" => ['2010-01-01', '2010-01-31']}, :journaled => Issue.last)
assert_match "01/01/2010", show_detail(@detail, true) assert_match "01/01/2010", show_detail(@journal, @journal.details.to_a.first, true)
end end
end end
context "with a due_date attribute" do context "with a due_date attribute" do
should "format the current date" do should "format the current date" do
@detail = JournalDetail.generate!(:property => 'attr', :old_value => '2010-01-01', :value => '2010-01-31', :prop_key => 'due_date') @journal = IssueJournal.generate!(:changes => {"due_date" => ['2010-01-01', '2010-01-31']}, :journaled => Issue.last)
assert_match "01/31/2010", show_detail(@detail, true) assert_match "01/31/2010", show_detail(@journal, @journal.details.to_a.first, true)
end end
should "format the old date" do should "format the old date" do
@detail = JournalDetail.generate!(:property => 'attr', :old_value => '2010-01-01', :value => '2010-01-31', :prop_key => 'due_date') @journal = IssueJournal.generate!(:changes => {"due_date" => ['2010-01-01', '2010-01-31']}, :journaled => Issue.last)
assert_match "01/01/2010", show_detail(@detail, true) assert_match "01/01/2010", show_detail(@journal, @journal.details.to_a.first, true)
end end
end end
......
...@@ -208,10 +208,8 @@ class IssueNestedSetTest < ActiveSupport::TestCase ...@@ -208,10 +208,8 @@ class IssueNestedSetTest < ActiveSupport::TestCase
issue3.save! issue3.save!
assert_difference 'Issue.count', -2 do assert_difference 'Issue.count', -2 do
assert_difference 'Journal.count', -1 do assert_difference 'IssueJournal.count', -3 do
assert_difference 'JournalDetail.count', -1 do Issue.find(issue2.id).destroy
Issue.find(issue2.id).destroy
end
end end
end end
...@@ -234,18 +232,17 @@ class IssueNestedSetTest < ActiveSupport::TestCase ...@@ -234,18 +232,17 @@ class IssueNestedSetTest < ActiveSupport::TestCase
end end
def test_destroy_child_issue_with_children def test_destroy_child_issue_with_children
root = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'root') root = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'root').reload
child = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => root.id) child = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => root.id).reload
leaf = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'leaf', :parent_issue_id => child.id) leaf = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'leaf', :parent_issue_id => child.id).reload
leaf.init_journal(User.find(2)) leaf.init_journal(User.find(2))
leaf.subject = 'leaf with journal' leaf.subject = 'leaf with journal'
leaf.save! leaf.save!
total_journals_on_children = leaf.reload.journals.count + child.reload.journals.count
assert_difference 'Issue.count', -2 do assert_difference 'Issue.count', -2 do
assert_difference 'Journal.count', -1 do assert_difference 'IssueJournal.count', -total_journals_on_children do
assert_difference 'JournalDetail.count', -1 do Issue.find(child.id).destroy
Issue.find(child.id).destroy
end
end end
end end
......
...@@ -630,41 +630,40 @@ class IssueTest < ActiveSupport::TestCase ...@@ -630,41 +630,40 @@ class IssueTest < ActiveSupport::TestCase
i.init_journal(User.find(2)) i.init_journal(User.find(2))
i.description = new_description i.description = new_description
assert_difference 'Journal.count', 1 do assert_difference 'IssueJournal.count', 1 do
assert_difference 'JournalDetail.count', 1 do i.save!
i.save!
end
end end
detail = JournalDetail.first(:order => 'id DESC') journal = IssueJournal.first(:order => 'id DESC')
assert_equal i, detail.journal.journalized assert_equal i, journal.journaled
assert_equal 'attr', detail.property assert journal.changes.has_key? "description"
assert_equal 'description', detail.prop_key assert_equal old_description, journal.old_value("description")
assert_equal old_description, detail.old_value assert_equal new_description, journal.value("description")
assert_equal new_description, detail.value
end end
# TODO: This test has become somewhat obsolete with the new journalized scheme
def test_saving_twice_should_not_duplicate_journal_details def test_saving_twice_should_not_duplicate_journal_details
i = Issue.find(:first) i = Issue.find(:first)
i.init_journal(User.find(2), 'Some notes') i.init_journal(User.find(2), 'Some notes')
# initial changes # initial changes
i.subject = 'New subject' i.subject = 'New subject'
i.done_ratio = i.done_ratio + 10 i.done_ratio = i.done_ratio + 10
assert_difference 'Journal.count' do assert_difference 'IssueJournal.count' do
assert i.save assert i.save
end end
assert i.current_journal.changes.has_key? "subject"
assert i.current_journal.changes.has_key? "done_ratio"
# 1 more change # 1 more change
i.priority = IssuePriority.find(:first, :conditions => ["id <> ?", i.priority_id]) i.priority = IssuePriority.find(:first, :conditions => ["id <> ?", i.priority_id])
assert_no_difference 'Journal.count' do assert_difference 'IssueJournal.count' do
assert_difference 'JournalDetail.count', 1 do i.save
i.save
end
end end
assert i.current_journal.changes.has_key? "priority_id"
# no more change # no more change
assert_no_difference 'Journal.count' do assert_no_difference 'IssueJournal.count' do
assert_no_difference 'JournalDetail.count' do i.save
i.save
end
end end
end end
......
...@@ -18,11 +18,15 @@ ...@@ -18,11 +18,15 @@
require File.expand_path('../../test_helper', __FILE__) require File.expand_path('../../test_helper', __FILE__)
class JournalObserverTest < ActiveSupport::TestCase class JournalObserverTest < ActiveSupport::TestCase
fixtures :issues, :issue_statuses, :journals, :journal_details fixtures :issues, :issue_statuses, :journals
def setup def setup
ActionMailer::Base.deliveries.clear ActionMailer::Base.deliveries.clear
@journal = Journal.find 1 @journal = Journal.find 1
if (i = Issue.find(:first)).journals.empty?
i.init_journal(User.current, 'Creation') # Make sure the initial journal is created
i.save
end
end end
# context: issue_updated notified_events # context: issue_updated notified_events
...@@ -30,9 +34,9 @@ class JournalObserverTest < ActiveSupport::TestCase ...@@ -30,9 +34,9 @@ class JournalObserverTest < ActiveSupport::TestCase
Setting.notified_events = ['issue_updated'] Setting.notified_events = ['issue_updated']
issue = Issue.find(:first) issue = Issue.find(:first)
user = User.find(:first) user = User.find(:first)
journal = issue.init_journal(user, issue) issue.init_journal(user)
assert journal.save assert issue.send(:create_journal)
assert_equal 1, ActionMailer::Base.deliveries.size assert_equal 1, ActionMailer::Base.deliveries.size
end end
...@@ -40,9 +44,9 @@ class JournalObserverTest < ActiveSupport::TestCase ...@@ -40,9 +44,9 @@ class JournalObserverTest < ActiveSupport::TestCase
Setting.notified_events = [] Setting.notified_events = []
issue = Issue.find(:first) issue = Issue.find(:first)
user = User.find(:first) user = User.find(:first)
journal = issue.init_journal(user, issue) issue.init_journal(user)
assert journal.save assert issue.save
assert_equal 0, ActionMailer::Base.deliveries.size assert_equal 0, ActionMailer::Base.deliveries.size
end end
...@@ -51,10 +55,9 @@ class JournalObserverTest < ActiveSupport::TestCase ...@@ -51,10 +55,9 @@ class JournalObserverTest < ActiveSupport::TestCase
Setting.notified_events = ['issue_note_added'] Setting.notified_events = ['issue_note_added']
issue = Issue.find(:first) issue = Issue.find(:first)
user = User.find(:first) user = User.find(:first)
journal = issue.init_journal(user, issue) issue.init_journal(user, 'This update has a note')
journal.notes = 'This update has a note'
assert journal.save assert issue.save
assert_equal 1, ActionMailer::Base.deliveries.size assert_equal 1, ActionMailer::Base.deliveries.size
end end
...@@ -62,10 +65,9 @@ class JournalObserverTest < ActiveSupport::TestCase ...@@ -62,10 +65,9 @@ class JournalObserverTest < ActiveSupport::TestCase
Setting.notified_events = [] Setting.notified_events = []
issue = Issue.find(:first) issue = Issue.find(:first)
user = User.find(:first) user = User.find(:first)
journal = issue.init_journal(user, issue) issue.init_journal(user, 'This update has a note')
journal.notes = 'This update has a note'
assert journal.save assert issue.save
assert_equal 0, ActionMailer::Base.deliveries.size assert_equal 0, ActionMailer::Base.deliveries.size
end end
...@@ -74,7 +76,7 @@ class JournalObserverTest < ActiveSupport::TestCase ...@@ -74,7 +76,7 @@ class JournalObserverTest < ActiveSupport::TestCase
Setting.notified_events = ['issue_status_updated'] Setting.notified_events = ['issue_status_updated']
issue = Issue.find(:first) issue = Issue.find(:first)
user = User.find(:first) user = User.find(:first)
issue.init_journal(user, issue) issue.init_journal(user)
issue.status = IssueStatus.last issue.status = IssueStatus.last
assert issue.save assert issue.save
...@@ -85,7 +87,7 @@ class JournalObserverTest < ActiveSupport::TestCase ...@@ -85,7 +87,7 @@ class JournalObserverTest < ActiveSupport::TestCase
Setting.notified_events = [] Setting.notified_events = []
issue = Issue.find(:first) issue = Issue.find(:first)
user = User.find(:first) user = User.find(:first)
issue.init_journal(user, issue) issue.init_journal(user)
issue.status = IssueStatus.last issue.status = IssueStatus.last
assert issue.save assert issue.save
...@@ -97,7 +99,7 @@ class JournalObserverTest < ActiveSupport::TestCase ...@@ -97,7 +99,7 @@ class JournalObserverTest < ActiveSupport::TestCase
Setting.notified_events = ['issue_priority_updated'] Setting.notified_events = ['issue_priority_updated']
issue = Issue.find(:first) issue = Issue.find(:first)
user = User.find(:first) user = User.find(:first)
issue.init_journal(user, issue) issue.init_journal(user)
issue.priority = IssuePriority.last issue.priority = IssuePriority.last
assert issue.save assert issue.save
...@@ -108,7 +110,7 @@ class JournalObserverTest < ActiveSupport::TestCase ...@@ -108,7 +110,7 @@ class JournalObserverTest < ActiveSupport::TestCase
Setting.notified_events = [] Setting.notified_events = []
issue = Issue.find(:first) issue = Issue.find(:first)
user = User.find(:first) user = User.find(:first)
issue.init_journal(user, issue) issue.init_journal(user)
issue.priority = IssuePriority.last issue.priority = IssuePriority.last
assert issue.save assert issue.save
......
...@@ -18,14 +18,14 @@ ...@@ -18,14 +18,14 @@
require File.expand_path('../../test_helper', __FILE__) require File.expand_path('../../test_helper', __FILE__)
class JournalTest < ActiveSupport::TestCase class JournalTest < ActiveSupport::TestCase
fixtures :projects, :issues, :issue_statuses, :journals, :journal_details, :users, :members, :member_roles fixtures :issues, :issue_statuses, :journals
def setup def setup
@journal = Journal.find 1 @journal = IssueJournal.find(1)
end end
def test_journalized_is_an_issue def test_journalized_is_an_issue
issue = @journal.issue issue = @journal.journalized
assert_kind_of Issue, issue assert_kind_of Issue, issue
assert_equal 1, issue.id assert_equal 1, issue.id
end end
...@@ -40,14 +40,18 @@ class JournalTest < ActiveSupport::TestCase ...@@ -40,14 +40,18 @@ class JournalTest < ActiveSupport::TestCase
def test_create_should_send_email_notification def test_create_should_send_email_notification
ActionMailer::Base.deliveries.clear ActionMailer::Base.deliveries.clear
issue = Issue.find(:first) issue = Issue.find(:first)
if issue.journals.empty?
issue.init_journal(User.current, "This journal represents the creational journal version 1")
issue.save
end
user = User.find(:first) user = User.find(:first)
journal = issue.init_journal(user, issue)
assert journal.save assert_equal 0, ActionMailer::Base.deliveries.size
issue.update_attribute(:subject, "New subject to trigger automatic journal entry")
assert_equal 1, ActionMailer::Base.deliveries.size assert_equal 1, ActionMailer::Base.deliveries.size
end end
def test_visible_scope_for_anonymous should_eventually "test_visible_scope_for_anonymous" do
# Anonymous user should see issues of public projects only # Anonymous user should see issues of public projects only
journals = Journal.visible(User.anonymous).all journals = Journal.visible(User.anonymous).all
assert journals.any? assert journals.any?
...@@ -57,8 +61,8 @@ class JournalTest < ActiveSupport::TestCase ...@@ -57,8 +61,8 @@ class JournalTest < ActiveSupport::TestCase
journals = Journal.visible(User.anonymous).all journals = Journal.visible(User.anonymous).all
assert journals.empty? assert journals.empty?
end end
def test_visible_scope_for_user should_eventually "test_visible_scope_for_user" do
user = User.find(9) user = User.find(9)
assert user.projects.empty? assert user.projects.empty?
# Non member user should see issues of public projects only # Non member user should see issues of public projects only
...@@ -78,7 +82,7 @@ class JournalTest < ActiveSupport::TestCase ...@@ -78,7 +82,7 @@ class JournalTest < ActiveSupport::TestCase
assert_nil journals.detect {|journal| journal.issue.project_id != 1} assert_nil journals.detect {|journal| journal.issue.project_id != 1}
end end
def test_visible_scope_for_admin should_eventually "test_visible_scope_for_admin" do
user = User.find(1) user = User.find(1)
user.members.each(&:destroy) user.members.each(&:destroy)
assert user.projects.empty? assert user.projects.empty?
...@@ -92,10 +96,12 @@ class JournalTest < ActiveSupport::TestCase ...@@ -92,10 +96,12 @@ class JournalTest < ActiveSupport::TestCase
ActionMailer::Base.deliveries.clear ActionMailer::Base.deliveries.clear
issue = Issue.find(:first) issue = Issue.find(:first)
user = User.find(:first) user = User.find(:first)
journal = issue.init_journal(user, issue) journal = issue.init_journal(user, "A note")
JournalObserver.instance.send_notification = false JournalObserver.instance.send_notification = false
assert journal.save assert_difference("Journal.count") do
assert issue.save
end
assert_equal 0, ActionMailer::Base.deliveries.size assert_equal 0, ActionMailer::Base.deliveries.size
end end
end end
...@@ -300,7 +300,7 @@ class MailHandlerTest < ActiveSupport::TestCase ...@@ -300,7 +300,7 @@ class MailHandlerTest < ActiveSupport::TestCase
journal = submit_email('ticket_reply.eml') journal = submit_email('ticket_reply.eml')
assert journal.is_a?(Journal) assert journal.is_a?(Journal)
assert_equal User.find_by_login('jsmith'), journal.user assert_equal User.find_by_login('jsmith'), journal.user
assert_equal Issue.find(2), journal.journalized assert_equal Issue.find(2), journal.journaled
assert_match /This is reply/, journal.notes assert_match /This is reply/, journal.notes
assert_equal 'Feature request', journal.issue.tracker.name assert_equal 'Feature request', journal.issue.tracker.name
end end
...@@ -309,9 +309,9 @@ class MailHandlerTest < ActiveSupport::TestCase ...@@ -309,9 +309,9 @@ class MailHandlerTest < ActiveSupport::TestCase
# This email contains: 'Status: Resolved' # This email contains: 'Status: Resolved'
journal = submit_email('ticket_reply_with_status.eml') journal = submit_email('ticket_reply_with_status.eml')
assert journal.is_a?(Journal) assert journal.is_a?(Journal)
issue = Issue.find(journal.issue.id) issue = Issue.find(journal.journaled.id)
assert_equal User.find_by_login('jsmith'), journal.user assert_equal User.find_by_login('jsmith'), journal.user
assert_equal Issue.find(2), journal.journalized assert_equal Issue.find(2), journal.journaled
assert_match /This is reply/, journal.notes assert_match /This is reply/, journal.notes
assert_equal 'Feature request', journal.issue.tracker.name assert_equal 'Feature request', journal.issue.tracker.name
assert_equal IssueStatus.find_by_name("Resolved"), issue.status assert_equal IssueStatus.find_by_name("Resolved"), issue.status
......
...@@ -172,7 +172,7 @@ class MailerTest < ActiveSupport::TestCase ...@@ -172,7 +172,7 @@ class MailerTest < ActiveSupport::TestCase
mail = ActionMailer::Base.deliveries.last mail = ActionMailer::Base.deliveries.last
assert_not_nil mail assert_not_nil mail
assert_equal Mailer.message_id_for(journal), mail.message_id assert_equal Mailer.message_id_for(journal), mail.message_id
assert_equal Mailer.message_id_for(journal.issue), mail.references.first.to_s assert_equal Mailer.message_id_for(journal.journaled), mail.references.first.to_s
end end
def test_message_posted_message_id def test_message_posted_message_id
......
...@@ -192,8 +192,7 @@ class ProjectTest < ActiveSupport::TestCase ...@@ -192,8 +192,7 @@ class ProjectTest < ActiveSupport::TestCase
assert_equal 0, Member.count, "Members were not deleted: #{Member.all.inspect}" assert_equal 0, Member.count, "Members were not deleted: #{Member.all.inspect}"
assert_equal 0, MemberRole.count assert_equal 0, MemberRole.count
assert_equal 0, Issue.count assert_equal 0, Issue.count
assert_equal 0, Journal.count assert_equal 0, IssueJournal.count
assert_equal 0, JournalDetail.count
assert_equal 0, Attachment.count assert_equal 0, Attachment.count
assert_equal 0, EnabledModule.count assert_equal 0, EnabledModule.count
assert_equal 0, IssueCategory.count assert_equal 0, IssueCategory.count
...@@ -212,7 +211,7 @@ class ProjectTest < ActiveSupport::TestCase ...@@ -212,7 +211,7 @@ class ProjectTest < ActiveSupport::TestCase
assert_equal 0, Wiki.count assert_equal 0, Wiki.count
assert_equal 0, WikiPage.count assert_equal 0, WikiPage.count
assert_equal 0, WikiContent.count assert_equal 0, WikiContent.count
assert_equal 0, WikiContent::Version.count assert_equal 0, WikiContentJournal.count
assert_equal 0, Project.connection.select_all("SELECT * FROM projects_trackers").size assert_equal 0, Project.connection.select_all("SELECT * FROM projects_trackers").size
assert_equal 0, Project.connection.select_all("SELECT * FROM custom_fields_projects").size assert_equal 0, Project.connection.select_all("SELECT * FROM custom_fields_projects").size
assert_equal 0, CustomValue.count(:conditions => {:customized_type => ['Project', 'Issue', 'TimeEntry', 'Version']}) assert_equal 0, CustomValue.count(:conditions => {:customized_type => ['Project', 'Issue', 'TimeEntry', 'Version']})
......
...@@ -249,7 +249,7 @@ class RepositoryGitTest < ActiveSupport::TestCase ...@@ -249,7 +249,7 @@ class RepositoryGitTest < ActiveSupport::TestCase
end end
def test_activities def test_activities
c = Changeset.new(:repository => @repository, c = Changeset.create(:repository => @repository,
:committed_on => Time.now, :committed_on => Time.now,
:revision => 'abc7234cb2750b63f47bff735edc50a1c0a433c2', :revision => 'abc7234cb2750b63f47bff735edc50a1c0a433c2',
:scmid => 'abc7234cb2750b63f47bff735edc50a1c0a433c2', :scmid => 'abc7234cb2750b63f47bff735edc50a1c0a433c2',
......
...@@ -228,7 +228,7 @@ class RepositoryMercurialTest < ActiveSupport::TestCase ...@@ -228,7 +228,7 @@ class RepositoryMercurialTest < ActiveSupport::TestCase
end end
def test_activities def test_activities
c = Changeset.new(:repository => @repository, c = Changeset.create(:repository => @repository,
:committed_on => Time.now, :committed_on => Time.now,
:revision => '123', :revision => '123',
:scmid => 'abc400bb8672', :scmid => 'abc400bb8672',
......
...@@ -128,14 +128,14 @@ class RepositorySubversionTest < ActiveSupport::TestCase ...@@ -128,14 +128,14 @@ class RepositorySubversionTest < ActiveSupport::TestCase
end end
def test_activities def test_activities
c = Changeset.new(:repository => @repository, :committed_on => Time.now, c = Changeset.create(:repository => @repository, :committed_on => Time.now,
:revision => '1', :comments => 'test') :revision => '1', :comments => 'test')
assert c.event_title.include?('1:') assert c.event_title.include?('1:')
assert_equal '1', c.event_url[:rev] assert_equal '1', c.event_url[:rev]
end end
def test_activities_nine_digit def test_activities_nine_digit
c = Changeset.new(:repository => @repository, :committed_on => Time.now, c = Changeset.create(:repository => @repository, :committed_on => Time.now,
:revision => '123456789', :comments => 'test') :revision => '123456789', :comments => 'test')
assert c.event_title.include?('123456789:') assert c.event_title.include?('123456789:')
assert_equal '123456789', c.event_url[:rev] assert_equal '123456789', c.event_url[:rev]
......
...@@ -97,7 +97,7 @@ class RepositoryTest < ActiveSupport::TestCase ...@@ -97,7 +97,7 @@ class RepositoryTest < ActiveSupport::TestCase
assert_equal [101], fixed_issue.changeset_ids assert_equal [101], fixed_issue.changeset_ids
# issue change # issue change
journal = fixed_issue.journals.find(:first, :order => 'created_on desc') journal = fixed_issue.journals.last
assert_equal User.find_by_login('dlopper'), journal.user assert_equal User.find_by_login('dlopper'), journal.user
assert_equal 'Applied in changeset r2.', journal.notes assert_equal 'Applied in changeset r2.', journal.notes
......
...@@ -27,7 +27,6 @@ class SearchTest < ActiveSupport::TestCase ...@@ -27,7 +27,6 @@ class SearchTest < ActiveSupport::TestCase
:issues, :issues,
:trackers, :trackers,
:journals, :journals,
:journal_details,
:repositories, :repositories,
:changesets :changesets
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
require File.expand_path('../../test_helper', __FILE__) require File.expand_path('../../test_helper', __FILE__)
class WikiContentTest < ActiveSupport::TestCase class WikiContentTest < ActiveSupport::TestCase
fixtures :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions, :users fixtures :wikis, :wiki_pages, :wiki_contents, :journals, :users
def setup def setup
@wiki = Wiki.find(1) @wiki = Wiki.find(1)
...@@ -72,9 +72,9 @@ class WikiContentTest < ActiveSupport::TestCase ...@@ -72,9 +72,9 @@ class WikiContentTest < ActiveSupport::TestCase
end end
def test_fetch_history def test_fetch_history
assert !@page.content.versions.empty? assert !@page.content.journals.empty?
@page.content.versions.each do |version| @page.content.journals.each do |journal|
assert_kind_of String, version.text assert_kind_of String, journal.text
end end
end end
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
require File.expand_path('../../test_helper', __FILE__) require File.expand_path('../../test_helper', __FILE__)
class WikiPageTest < ActiveSupport::TestCase class WikiPageTest < ActiveSupport::TestCase
fixtures :projects, :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions fixtures :projects, :wikis, :wiki_pages, :wiki_contents, :journals
def setup def setup
@wiki = Wiki.find(1) @wiki = Wiki.find(1)
...@@ -101,11 +101,14 @@ class WikiPageTest < ActiveSupport::TestCase ...@@ -101,11 +101,14 @@ class WikiPageTest < ActiveSupport::TestCase
def test_destroy def test_destroy
page = WikiPage.find(1) page = WikiPage.find(1)
content_ids = WikiContent.find_all_by_page_id(1).collect(&:id)
page.destroy page.destroy
assert_nil WikiPage.find_by_id(1) assert_nil WikiPage.find_by_id(1)
# make sure that page content and its history are deleted # make sure that page content and its history are deleted
assert WikiContent.find_all_by_page_id(1).empty? assert WikiContent.find_all_by_page_id(1).empty?
assert WikiContent.versioned_class.find_all_by_page_id(1).empty? content_ids.each do |wiki_content_id|
assert WikiContent.journal_class.find_all_by_journaled_id(wiki_content_id).empty?
end
end end
def test_destroy_should_not_nullify_children def test_destroy_should_not_nullify_children
......
...@@ -20,8 +20,8 @@ ...@@ -20,8 +20,8 @@
require File.expand_path('../../test_helper', __FILE__) require File.expand_path('../../test_helper', __FILE__)
class WikiTest < ActiveSupport::TestCase class WikiTest < ActiveSupport::TestCase
fixtures :projects, :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions fixtures :projects, :wikis, :wiki_pages, :wiki_contents, :journals
def test_create def test_create
wiki = Wiki.new(:project => Project.find(2)) wiki = Wiki.new(:project => Project.find(2))
assert !wiki.save assert !wiki.save
......
...@@ -68,12 +68,12 @@ module Redmine ...@@ -68,12 +68,12 @@ module Redmine
scope_options[:conditions] = cond.conditions scope_options[:conditions] = cond.conditions
if options[:limit] if options[:limit]
# id and creation time should be in same order in most cases # id and creation time should be in same order in most cases
scope_options[:order] = "#{table_name}.id DESC" scope_options[:order] = "#{journal_class.table_name}.id DESC"
scope_options[:limit] = options[:limit] scope_options[:limit] = options[:limit]
end end
with_scope(:find => scope_options) do journal_class.with_scope(:find => scope_options) do
find(:all, provider_options[:find_options].dup) journal_class.find(:all, provider_options[:find_options].dup)
end end
end end
end end
......
## MAC OS
.DS_Store
## TEXTMATE
*.tmproj
tmtags
## EMACS
*~
\#*
.\#*
## VIM
*.swp
## PROJECT::GENERAL
coverage
rdoc
pkg
## PROJECT::SPECIFIC
*.db
This diff is collapsed.
"Acts_as_journalized" is a Redmine core plugin derived from the vestal_versions
Ruby on Rails plugin. The parts are under different copyright and license conditions
noted below.
The overall license terms applying to "Acts_as_journalized" as in
this distribution are the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2 of
the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
For the individual files, the following copyrights and licenses apply:
app/controllers/**
app/views/**
app/helpers/**
app/models/journal_observer.rb
Copyright (C) 2006-2008 Jean-Philippe Lang
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
lib/acts_as_journalized.rb
lib/journal_formatter.rb
lib/redmine/acts/journalized/permissions.rb
lib/redmine/acts/journalized/save_hooks.rb
lib/redmine/acts/journalized/format_hooks.rb
lib/redmine/acts/journalized/deprecated.rb
Copyright (c) 2010 Finn GmbH
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
All remaining files are:
Copyright (c) 2009 Steve Richert
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
acts as journalized
A redmine core plugin for unification of journals, events and activities in redmine
67a8c4bee0a06420f1ba64eb9906a15d63bf5ac5 https://github.com/edavis10/acts_as_journalized
# This file is part of the acts_as_journalized plugin for the redMine
# project management software
#
# Copyright (C) 2006-2008 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either journal 2
# of the License, or (at your option) any later journal.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class JournalsController < ApplicationController
unloadable
before_filter :find_journal
def edit
if request.post?
@journal.update_attribute(:notes, params[:notes]) if params[:notes]
@journal.destroy if @journal.details.empty? && @journal.notes.blank?
call_hook(:controller_journals_edit_post, { :journal => @journal, :params => params})
respond_to do |format|
format.html { redirect_to :controller => @journal.journaled.class.name.pluralize.downcase,
:action => 'show', :id => @journal.journaled_id }
format.js { render :action => 'update' }
end
end
end
private
def find_journal
@journal = Journal.find(params[:id])
(render_403; return false) unless @journal.editable_by?(User.current)
@project = @journal.project
rescue ActiveRecord::RecordNotFound
render_404
end
end
# This file is part of the acts_as_journalized plugin for the redMine
# project management software
#
# Copyright (C) 2006-2008 Jean-Philippe Lang
# Copyright (C) 2010 Finn GmbH, http://finn.de
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either journal 2
# of the License, or (at your option) any later journal.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module JournalsHelper
unloadable
include ApplicationHelper
include ActionView::Helpers::TagHelper
def self.included(base)
base.class_eval do
if respond_to? :before_filter
before_filter :find_optional_journal, :only => [:edit]
end
end
end
def render_journal(model, journal, options = {})
return "" if journal.initial?
journal_content = render_journal_details(journal, :label_updated_time_by)
journal_content += render_notes(model, journal, options) unless journal.notes.blank?
content_tag "div", journal_content, { :id => "change-#{journal.id}", :class => journal.css_classes }
end
# This renders a journal entry wiht a header and details
def render_journal_details(journal, header_label = :label_updated_time_by)
header = <<-HTML
<h4>
<div style="float:right;">#{link_to "##{journal.anchor}", :anchor => "note-#{journal.anchor}"}</div>
#{avatar(journal.user, :size => "24")}
#{content_tag('a', '', :name => "note-#{journal.anchor}")}
#{authoring journal.created_at, journal.user, :label => header_label}
</h4>
HTML
if journal.details.any?
details = content_tag "ul", :class => "details" do
journal.details.collect do |detail|
if d = journal.render_detail(detail)
content_tag("li", d)
end
end.compact
end
end
content_tag("div", "#{header}#{details}", :id => "change-#{journal.id}", :class => "journal")
end
def render_notes(model, journal, options={})
controller = model.class.name.downcase.pluralize
action = 'edit'
reply_links = authorize_for(controller, action)
if User.current.logged?
editable = User.current.allowed_to?(options[:edit_permission], journal.project) if options[:edit_permission]
if journal.user == User.current && options[:edit_own_permission]
editable ||= User.current.allowed_to?(options[:edit_own_permission], journal.project)
end
end
unless journal.notes.blank?
links = returning [] do |l|
if reply_links
l << link_to_remote(image_tag('comment.png'), :title => l(:button_quote),
:url => {:controller => controller, :action => action, :id => model, :journal_id => journal})
end
if editable
l << link_to_in_place_notes_editor(image_tag('edit.png'), "journal-#{journal.id}-notes",
{ :controller => 'journals', :action => 'edit', :id => journal },
:title => l(:button_edit))
end
end
end
content = ''
content << content_tag('div', links.join(' '), :class => 'contextual') unless links.empty?
content << textilizable(journal, :notes)
css_classes = "wiki"
css_classes << " editable" if editable
content_tag('div', content, :id => "journal-#{journal.id}-notes", :class => css_classes)
end
def link_to_in_place_notes_editor(text, field_id, url, options={})
onclick = "new Ajax.Request('#{url_for(url)}', {asynchronous:true, evalScripts:true, method:'get'}); return false;"
link_to text, '#', options.merge(:onclick => onclick)
end
# This may conveniently be used by controllers to find journals referred to in the current request
def find_optional_journal
@journal = Journal.find_by_id(params[:journal_id])
end
def render_reply(journal)
user = journal.user
text = journal.notes
# Replaces pre blocks with [...]
text = text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]')
content = "#{ll(Setting.default_language, :text_user_wrote, user)}\n> "
content << text.gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n"
render(:update) do |page|
page << "$('notes').value = \"#{escape_javascript content}\";"
page.show 'update'
page << "Form.Element.focus('notes');"
page << "Element.scrollTo('update');"
page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
end
end
end
\ No newline at end of file
# This file is part of the acts_as_journalized plugin for the redMine
# project management software
#
# Copyright (c) 2009 Steve Richert
# Copyright (c) 2010 Finn GmbH, http://finn.de
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either journal 2
# of the License, or (at your option) any later journal.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require_dependency 'journal_formatter'
# The ActiveRecord model representing journals.
class Journal < ActiveRecord::Base
unloadable
include Comparable
include JournalFormatter
include JournalDeprecated
# Make sure each journaled model instance only has unique version ids
validates_uniqueness_of :version, :scope => [:journaled_id, :type]
belongs_to :journaled
belongs_to :user
# ActiveRecord::Base#changes is an existing method, so before serializing the +changes+ column,
# the existing +changes+ method is undefined. The overridden +changes+ method pertained to
# dirty attributes, but will not affect the partial updates functionality as that's based on
# an underlying +changed_attributes+ method, not +changes+ itself.
# undef_method :changes
serialize :changes, Hash
# In conjunction with the included Comparable module, allows comparison of journal records
# based on their corresponding version numbers, creation timestamps and IDs.
def <=>(other)
[version, created_at, id].map(&:to_i) <=> [other.version, other.created_at, other.id].map(&:to_i)
end
# Returns whether the version has a version number of 1. Useful when deciding whether to ignore
# the version during reversion, as initial versions have no serialized changes attached. Helps
# maintain backwards compatibility.
def initial?
version < 2
end
# The anchor number for html output
def anchor
version - 1
end
# Possible shortcut to the associated project
def project
if journaled.respond_to?(:project)
journaled.project
elsif journaled.is_a? Project
journaled
else
nil
end
end
def editable_by?(user)
journaled.journal_editable_by?(user)
end
def details
attributes["changes"] || {}
end
alias_method :changes, :details
def new_value_for(prop)
details[prop.to_s].last if details.keys.include? prop.to_s
end
def old_value_for(prop)
details[prop.to_s].first if details.keys.include? prop.to_s
end
# Returns a string of css classes
def css_classes
s = 'journal'
s << ' has-notes' unless notes.blank?
s << ' has-details' unless details.empty?
s
end
# This is here to allow people to disregard the difference between working with a
# Journal and the object it is attached to.
# The lookup is as follows:
## => Call super if the method corresponds to one of our attributes (will end up in AR::Base)
## => Try the journaled object with the same method and arguments
## => On error, call super
def method_missing(method, *args, &block)
return super if attributes[method.to_s]
journaled.send(method, *args, &block)
rescue NoMethodError => e
e.name == method ? super : raise(e)
end
end
...@@ -17,18 +17,24 @@ ...@@ -17,18 +17,24 @@
class JournalObserver < ActiveRecord::Observer class JournalObserver < ActiveRecord::Observer
attr_accessor :send_notification attr_accessor :send_notification
def after_create(journal) def after_create(journal)
if journal.type == "IssueJournal" and journal.version > 1 and self.send_notification
after_create_issue_journal(journal)
end
clear_notification
end
def after_create_issue_journal(journal)
if Setting.notified_events.include?('issue_updated') || if Setting.notified_events.include?('issue_updated') ||
(Setting.notified_events.include?('issue_note_added') && journal.notes.present?) || (Setting.notified_events.include?('issue_note_added') && journal.notes.present?) ||
(Setting.notified_events.include?('issue_status_updated') && journal.new_status.present?) || (Setting.notified_events.include?('issue_status_updated') && journal.new_status.present?) ||
(Setting.notified_events.include?('issue_priority_updated') && journal.new_value_for('priority_id').present?) (Setting.notified_events.include?('issue_priority_updated') && journal.new_value_for('priority_id').present?)
Mailer.deliver_issue_edit(journal) if self.send_notification Mailer.deliver_issue_edit(journal)
end end
clear_notification
end end
# Wrap send_notification so it defaults to true, when it's nil # Wrap send_notification so it defaults to true, when it's nil
def send_notification def send_notification
return true if @send_notification.nil? return true if @send_notification.nil?
return @send_notification return @send_notification
...@@ -40,4 +46,5 @@ class JournalObserver < ActiveRecord::Observer ...@@ -40,4 +46,5 @@ class JournalObserver < ActiveRecord::Observer
def clear_notification def clear_notification
@send_notification = true @send_notification = true
end end
end end
<% form_remote_tag(:url => {}, :html => { :id => "journal-#{@journal.id}-form" }) do %> <% form_remote_tag(:url => {}, :html => { :id => "journal-#{@journal.id}-form" }) do %>
<%= text_area_tag :notes, @journal.notes, <%= text_area_tag :notes, @journal.notes, :class => 'wiki-edit',
:id => "journal_#{@journal.id}_notes", :rows => (@journal.notes.blank? ? 10 : [[10, @journal.notes.length / 50].max, 100].min) %>
:class => 'wiki-edit',
:rows => (@journal.notes.blank? ? 10 : [[10, @journal.notes.length / 50].max, 100].min) %>
<%= call_hook(:view_journals_notes_form_after_notes, { :journal => @journal}) %> <%= call_hook(:view_journals_notes_form_after_notes, { :journal => @journal}) %>
<p><%= submit_tag l(:button_save) %> <p><%= submit_tag l(:button_save) %>
<%= link_to_remote l(:label_preview),
{ :url => preview_issue_path(:project_id => @project, :id => @journal.issue),
:method => 'post',
:update => "journal_#{@journal.id}_preview",
:with => "Form.serialize('journal-#{@journal.id}-form')",
:complete => "Element.scrollTo('journal_#{@journal.id}_preview')"
}, :accesskey => accesskey(:preview) %>
|
<%= link_to l(:button_cancel), '#', :onclick => "Element.remove('journal-#{@journal.id}-form'); " + <%= link_to l(:button_cancel), '#', :onclick => "Element.remove('journal-#{@journal.id}-form'); " +
"Element.show('journal-#{@journal.id}-notes'); return false;" %></p> "Element.show('journal-#{@journal.id}-notes'); return false;" %></p>
<div id="journal_<%= @journal.id %>_preview" class="wiki"></div>
<% end %> <% end %>
<%= wikitoolbar_for "journal_#{@journal.id}_notes" %>
...@@ -2,7 +2,8 @@ if @journal.frozen? ...@@ -2,7 +2,8 @@ if @journal.frozen?
# journal was destroyed # journal was destroyed
page.remove "change-#{@journal.id}" page.remove "change-#{@journal.id}"
else else
page.replace "journal-#{@journal.id}-notes", render_notes(@journal.issue, @journal, :reply_links => authorize_for('issues', 'edit')) page.replace "journal-#{@journal.id}-notes", render_notes(@journal.journaled, @journal,
:edit_permission => :edit_issue_notes, :edit_own_permission => :edit_own_issue_notes)
page.show "journal-#{@journal.id}-notes" page.show "journal-#{@journal.id}-notes"
page.remove "journal-#{@journal.id}-form" page.remove "journal-#{@journal.id}-form"
end end
......
$LOAD_PATH.unshift File.expand_path("../lib/", __FILE__)
require "acts_as_journalized"
ActiveRecord::Base.send(:include, Redmine::Acts::Journalized)
require 'dispatcher'
Dispatcher.to_prepare do
# Model
require_dependency "journal"
# this is for compatibility with current trunk
# once the plugin is part of the core, this will not be needed
# patches should then be ported onto the core
# require_dependency File.dirname(__FILE__) + '/lib/acts_as_journalized/journal_patch'
# require_dependency File.dirname(__FILE__) + '/lib/acts_as_journalized/journal_observer_patch'
# require_dependency File.dirname(__FILE__) + '/lib/acts_as_journalized/activity_fetcher_patch'
end
# This file is part of the acts_as_journalized plugin for the redMine
# project management software
#
# Copyright (C) 2010 Finn GmbH, http://finn.de
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either journal 2
# of the License, or (at your option) any later journal.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
Dir[File.expand_path("../redmine/acts/journalized/*.rb", __FILE__)].each{|f| require f }
require_dependency 'lib/ar_condition'
module Redmine
module Acts
module Journalized
def self.included(base)
base.extend ClassMethods
base.extend Versioned
end
module ClassMethods
def plural_name
self.name.underscore.pluralize
end
# A model might provide as many activity_types as it wishes.
# Activities are just different search options for the event a model provides
def acts_as_activity(options = {})
activity_hash = journalized_activity_hash(options)
type = activity_hash[:type]
acts_as_activity_provider activity_hash
unless Redmine::Activity.providers[type].include? self.name
Redmine::Activity.register type.to_sym, :class_name => self.name
end
end
# This call will add an activity and, if neccessary, start the journaling and
# add an event callback on the model.
# Versioning and acting as an Event may only be applied once.
# To apply more than on activity, use acts_as_activity
def acts_as_journalized(options = {}, &block)
activity_hash, event_hash, journal_hash = split_option_hashes(options)
acts_as_activity(activity_hash)
return if journaled?
include Options
include Changes
include Creation
include Users
include Reversion
include Reset
include Reload
include Permissions
include SaveHooks
include FormatHooks
# FIXME: When the transition to the new API is complete, remove me
include Deprecated
journal_class.acts_as_event journalized_event_hash(event_hash)
(journal_hash[:except] ||= []) << self.primary_key << inheritance_column <<
:updated_on << :updated_at << :lock_version << :lft << :rgt
prepare_journaled_options(journal_hash)
has_many :journals, journal_hash.merge({:class_name => journal_class.name,
:foreign_key => "journaled_id"}), &block
end
def journal_class
journal_class_name = "#{name.gsub("::", "_")}Journal"
if Object.const_defined?(journal_class_name)
Object.const_get(journal_class_name)
else
Object.const_set(journal_class_name, Class.new(Journal)).tap do |c|
# Run after the inherited hook to associate with the parent record.
# This eager loads the associated project (for permissions) if possible
if project_assoc = reflect_on_association(:project).try(:name)
include_option = ", :include => :#{project_assoc.to_s}"
end
c.class_eval("belongs_to :journaled, :class_name => '#{name}' #{include_option}")
c.class_eval("belongs_to :#{name.gsub("::", "_").underscore},
:foreign_key => 'journaled_id' #{include_option}")
end
end
end
private
# Splits an option has into three hashes:
## => [{ options prefixed with "activity_" }, { options prefixed with "event_" }, { other options }]
def split_option_hashes(options)
activity_hash = {}
event_hash = {}
journal_hash = {}
options.each_pair do |k, v|
case
when k.to_s =~ /^activity_(.+)$/
activity_hash[$1.to_sym] = v
when k.to_s =~ /^event_(.+)$/
event_hash[$1.to_sym] = v
else
journal_hash[k.to_sym] = v
end
end
[activity_hash, event_hash, journal_hash]
end
# Merges the passed activity_hash with the options we require for
# acts_as_journalized to work, as follows:
# # type is the supplied or the pluralized class name
# # timestamp is supplied or the journal's created_at
# # author_key will always be the journal's author
# #
# # find_options are merged as follows:
# # # select statement is enriched with the journal fields
# # # journal association is added to the includes
# # # if a project is associated with the model, this is added to the includes
# # # the find conditions are extended to only choose journals which have the proper activity_type
# => a valid activity hash
def journalized_activity_hash(options)
options.tap do |h|
h[:type] ||= plural_name
h[:timestamp] = "#{journal_class.table_name}.created_at"
h[:author_key] = "#{journal_class.table_name}.user_id"
h[:find_options] ||= {} # in case it is nil
h[:find_options] = {}.tap do |opts|
cond = ARCondition.new
cond.add(["#{journal_class.table_name}.activity_type = ?", h[:type]])
cond.add(h[:find_options][:conditions]) if h[:find_options][:conditions]
opts[:conditions] = cond.conditions
include_opts = []
include_opts << :project if reflect_on_association(:project)
if h[:find_options][:include]
include_opts += case h[:find_options][:include]
when Array then h[:find_options][:include]
else [h[:find_options][:include]]
end
end
include_opts.uniq!
opts[:include] = [:journaled => include_opts]
#opts[:joins] = h[:find_options][:joins] if h[:find_options][:joins]
end
end
end
# Merges the event hashes defaults with the options provided by the user
# The defaults take their details from the journal
def journalized_event_hash(options)
unless options.has_key? :url
options[:url] = Proc.new do |journal|
{ :controller => plural_name,
:action => 'show',
:id => journal.journaled_id,
:anchor => ("note-#{journal.anchor}" unless journal.initial?) }
end
end
{ :description => :notes, :author => :user }.reverse_merge options
end
end
end
end
end
\ No newline at end of file
# This file is part of the acts_as_journalized plugin for the redMine
# project management software
#
# Copyright (C) 2010 Finn GmbH, http://finn.de
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either journal 2
# of the License, or (at your option) any later journal.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# This module holds the formatting methods that each journal has.
# It provides the hooks to apply different formatting to the details
# of a specific journal.
module JournalDeprecated
unloadable
# Old timestamps. created_at is what t.timestamps creates in recent Rails journals
def created_on
created_at
end
# Old naming
def journalized
journaled
end
# Old naming
def journalized= obj
journaled = obj
end
# Shortcut from more issue-specific journals
def attachments
journalized.respond_to?(:attachments) ? journalized.attachments : nil
end
# deprecate :created_on => "use #created_at"
# deprecate :journalized => "use journaled"
# deprecate :attachments => "implement it yourself"
end
# This file is part of the acts_as_journalized plugin for the redMine
# project management software
#
# Copyright (C) 2010 Finn GmbH, http://finn.de
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either journal 2
# of the License, or (at your option) any later journal.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# This module holds the formatting methods that each journal has.
# It provides the hooks to apply different formatting to the details
# of a specific journal.
module JournalFormatter
unloadable
mattr_accessor :formatters, :registered_fields
include ApplicationHelper
include CustomFieldsHelper
include ActionView::Helpers::TagHelper
include ActionView::Helpers::UrlHelper
extend Redmine::I18n
def self.register(hash)
if hash[:class]
klazz = hash.delete(:class)
registered_fields[klazz] ||= {}
registered_fields[klazz].merge!(hash)
else
formatters.merge(hash)
end
end
# TODO: Document Formatters (can take up to three params, value, journaled, field ...)
def self.default_formatters
{ :plaintext => (Proc.new {|v,*| v.try(:to_s) }),
:datetime => (Proc.new {|v,*| format_date(v.to_date) }),
:named_association => (Proc.new do |value, journaled, field|
association = journaled.class.reflect_on_association(field.to_sym)
if association
record = association.class_name.constantize.find_by_id(value.to_i)
record.name if record
end
end),
:fraction => (Proc.new {|v,*| "%0.02f" % v.to_f }),
:decimal => (Proc.new {|v,*| v.to_i.to_s }),
:id => (Proc.new {|v,*| "##{v}" }) }
end
self.formatters = default_formatters
self.registered_fields = {}
def format_attribute_detail(key, values, no_html=false)
field = key.to_s.gsub(/\_id$/, "")
label = l(("field_" + field).to_sym)
if format = JournalFormatter.registered_fields[self.class.name.to_sym][key]
formatter = JournalFormatter.formatters[format]
old_value = formatter.call(values.first, journaled, field) if values.first
value = formatter.call(values.last, journaled, field) if values.last
[label, old_value, value]
else
return nil
end
end
def format_custom_value_detail(custom_field, values, no_html)
label = custom_field.name
old_value = format_value(values.first, custom_field.field_format) if values.first
value = format_value(values.last, custom_field.field_format) if values.last
[label, old_value, value]
end
def format_attachment_detail(key, values, no_html)
label = l(:label_attachment)
old_value = values.first
value = values.last
[label, old_value, value]
end
def format_html_attachment_detail(key, value)
if !value.blank? && a = Attachment.find_by_id(key.to_i)
# Link to the attachment if it has not been removed
# FIXME: this is broken => link_to_attachment(a)
a.filename
else
content_tag("i", h(value)) if value.present?
end
end
def format_html_detail(label, old_value, value)
label = content_tag('strong', label)
old_value = content_tag("i", h(old_value)) if old_value && !old_value.blank?
old_value = content_tag("strike", old_value) if old_value and value.blank?
value = content_tag("i", h(value)) if value.present?
value ||= ""
[label, old_value, value]
end
def property(detail)
key = prop_key(detail)
if key.start_with? "custom_values"
:custom_field
elsif key.start_with? "attachments"
:attachment
elsif journaled.class.columns.collect(&:name).include? key
:attribute
end
end
def prop_key(detail)
if detail.respond_to? :to_ary
detail.first
else
detail
end
end
def values(detail)
key = prop_key(detail)
if detail != key
detail.last
else
details[key.to_s]
end
end
def old_value(detail)
values(detail).first
end
def value(detail)
values(detail).last
end
def render_detail(detail, no_html=false)
if detail.respond_to? :to_ary
key = detail.first
values = detail.last
else
key = detail
values = details[key.to_s]
end
case property(detail)
when :attribute
attr_detail = format_attribute_detail(key, values, no_html)
when :custom_field
custom_field = CustomField.find_by_id(key.sub("custom_values", "").to_i)
cv_detail = format_custom_value_detail(custom_field, values, no_html)
when :attachment
attachment_detail = format_attachment_detail(key.sub("attachments", ""), values, no_html)
end
label, old_value, value = attr_detail || cv_detail || attachment_detail
Redmine::Hook.call_hook :helper_issues_show_detail_after_setting, {:detail => detail,
:label => label, :value => value, :old_value => old_value }
return nil unless label || old_value || value # print nothing if there are no values
label, old_value, value = [label, old_value, value].collect(&:to_s)
unless no_html
label, old_value, value = *format_html_detail(label, old_value, value)
value = format_html_attachment_detail(key.sub("attachments", ""), value) if attachment_detail
end
unless value.blank?
if attr_detail || cv_detail
unless old_value.blank?
l(:text_journal_changed, :label => label, :old => old_value, :new => value)
else
l(:text_journal_set_to, :label => label, :value => value)
end
elsif attachment_detail
l(:text_journal_added, :label => label, :value => value)
end
else
l(:text_journal_deleted, :label => label, :old => old_value)
end
end
end
# This file included as part of the acts_as_journalized plugin for
# the redMine project management software; You can redistribute it
# and/or modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either journal 2
# of the License, or (at your option) any later journal.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# The original copyright and license conditions are:
# Copyright (c) 2009 Steve Richert
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
module Redmine::Acts::Journalized
# Provides the ability to manipulate hashes in the specific format that ActiveRecord gives to
# dirty attribute changes: string keys and unique, two-element array values.
module Changes
def self.included(base) # :nodoc:
Hash.send(:include, HashMethods)
base.class_eval do
include InstanceMethods
after_update :merge_journal_changes
end
end
# Methods available to journaled ActiveRecord::Base instances in order to manage changes used
# for journal creation.
module InstanceMethods
# Collects an array of changes from a record's journals between the given range and compiles
# them into one summary hash of changes. The +from+ and +to+ arguments can each be either a
# version number, a symbol representing an association proxy method, a string representing a
# journal tag or a journal object itself.
def changes_between(from, to)
from_number, to_number = journals.journal_at(from), journals.journal_at(to)
return {} if from_number == to_number
chain = journals.between(from_number, to_number).reject(&:initial?)
return {} if chain.empty?
backward = from_number > to_number
backward ? chain.pop : chain.shift unless from_number == 1 || to_number == 1
chain.inject({}) do |changes, journal|
changes.append_changes!(backward ? journal.changes.reverse_changes : journal.changes)
end
end
private
# Before a new journal is created, the newly-changed attributes are appended onto a hash
# of previously-changed attributes. Typically the previous changes will be empty, except in
# the case that a control block is used where journals are to be merged. See
# VestalVersions::Control for more information.
def merge_journal_changes
journal_changes.append_changes!(incremental_journal_changes)
end
# Stores the cumulative changes that are eventually used for journal creation.
def journal_changes
@journal_changes ||= {}
end
# Stores the incremental changes that are appended to the cumulative changes before journal
# creation. Incremental changes are reset when the record is saved because they represent
# a subset of the dirty attribute changes, which are reset upon save.
def incremental_journal_changes
changes.slice(*journaled_columns)
end
# Simply resets the cumulative changes after journal creation.
def reset_journal_changes
@journal_changes = nil
end
end
# Instance methods included into Hash for dealing with manipulation of hashes in the specific
# format of ActiveRecord::Base#changes.
module HashMethods
# When called on a hash of changes and given a second hash of changes as an argument,
# +append_changes+ will run the second hash on top of the first, updating the last element
# of each array value with its own, or creating its own key/value pair for missing keys.
# Resulting non-unique array values are removed.
#
# == Example
#
# first = {
# "first_name" => ["Steve", "Stephen"],
# "age" => [25, 26]
# }
# second = {
# "first_name" => ["Stephen", "Steve"],
# "last_name" => ["Richert", "Jobs"],
# "age" => [26, 54]
# }
# first.append_changes(second)
# # => {
# "last_name" => ["Richert", "Jobs"],
# "age" => [25, 54]
# }
def append_changes(changes)
changes.inject(self) do |new_changes, (attribute, change)|
new_change = [new_changes.fetch(attribute, change).first, change.last]
new_changes.merge(attribute => new_change)
end.reject do |attribute, change|
change.first == change.last
end
end
# Destructively appends a given hash of changes onto an existing hash of changes.
def append_changes!(changes)
replace(append_changes(changes))
end
# Appends the existing hash of changes onto a given hash of changes. Relates to the
# +append_changes+ method in the same way that Hash#reverse_merge relates to
# Hash#merge.
def prepend_changes(changes)
changes.append_changes(self)
end
# Destructively prepends a given hash of changes onto an existing hash of changes.
def prepend_changes!(changes)
replace(prepend_changes(changes))
end
# Reverses the array values of a hash of changes. Useful for rejournal both backward and
# forward through a record's history of changes.
def reverse_changes
inject({}){|nc,(a,c)| nc.merge!(a => c.reverse) }
end
# Destructively reverses the array values of a hash of changes.
def reverse_changes!
replace(reverse_changes)
end
end
end
end
# This file included as part of the acts_as_journalized plugin for
# the redMine project management software; You can redistribute it
# and/or modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# The original copyright and license conditions are:
# Copyright (c) 2009 Steve Richert
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
module Redmine::Acts::Journalized
# Allows for easy application-wide configuration of options passed into the +journaled+ method.
module Configuration
# The VestalVersions module is extended by VestalVersions::Configuration, allowing the
# +configure method+ to be used as follows in a Rails initializer:
#
# VestalVersions.configure do |config|
# config.class_name = "MyCustomVersion"
# config.dependent = :destroy
# end
#
# Each variable assignment in the +configure+ block corresponds directly with the options
# available to the +journaled+ method. Assigning common options in an initializer can keep your
# models tidy.
#
# If an option is given in both an initializer and in the options passed to +journaled+, the
# value given in the model itself will take precedence.
def configure
yield Configuration
end
class << self
# Simply stores a hash of options given to the +configure+ block.
def options
@options ||= {}
end
# If given a setter method name, will assign the first argument to the +options+ hash with
# the method name (sans "=") as the key. If given a getter method name, will attempt to
# a value from the +options+ hash for that key. If the key doesn't exist, defers to +super+.
def method_missing(symbol, *args)
if (method = symbol.to_s).sub!(/\=$/, '')
options[method.to_sym] = args.first
else
options.fetch(method.to_sym, super)
end
end
end
end
end
# This file included as part of the acts_as_journalized plugin for
# the redMine project management software; You can redistribute it
# and/or modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# The original copyright and license conditions are:
# Copyright (c) 2009 Steve Richert
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
module Redmine::Acts::Journalized
# Adds the functionality necessary to control journal creation on a journaled instance of
# ActiveRecord::Base.
module Creation
def self.included(base) # :nodoc:
base.class_eval do
extend ClassMethods
include InstanceMethods
after_save :create_journal, :if => :create_journal?
class << self
alias_method_chain :prepare_journaled_options, :creation
end
end
end
# Class methods added to ActiveRecord::Base to facilitate the creation of new journals.
module ClassMethods
# Overrides the basal +prepare_journaled_options+ method defined in VestalVersions::Options
# to extract the <tt>:only</tt> and <tt>:except</tt> options into +vestal_journals_options+.
def prepare_journaled_options_with_creation(options)
result = prepare_journaled_options_without_creation(options)
self.vestal_journals_options[:only] = Array(options.delete(:only)).map(&:to_s).uniq if options[:only]
self.vestal_journals_options[:except] = Array(options.delete(:except)).map(&:to_s).uniq if options[:except]
result
end
end
# Instance methods that determine whether to save a journal and actually perform the save.
module InstanceMethods
private
# Returns whether a new journal should be created upon updating the parent record.
# A new journal will be created if
# a) attributes have changed
# b) no previous journal exists
# c) journal notes were added
# d) the parent record is already saved
def create_journal?
update_journal
(journal_changes.present? or journal_notes.present? or journals.empty?) and !new_record?
end
# Creates a new journal upon updating the parent record.
# "update_journal" has been called in "update_journal?" at this point (to get a hold on association changes)
# It must not be called again here.
def create_journal
journals << self.class.journal_class.create(journal_attributes)
reset_journal_changes
reset_journal
true
rescue Exception => e # FIXME: What to do? This likely means that the parent record is invalid!
p e
p e.message
p e.backtrace
false
end
# Returns an array of column names that should be included in the changes of created
# journals. If <tt>vestal_journals_options[:only]</tt> is specified, only those columns
# will be journaled. Otherwise, if <tt>vestal_journals_options[:except]</tt> is specified,
# all columns will be journaled other than those specified. Without either option, the
# default is to journal all columns. At any rate, the four "automagic" timestamp columns
# maintained by Rails are never journaled.
def journaled_columns
case
when vestal_journals_options[:only] then self.class.column_names & vestal_journals_options[:only]
when vestal_journals_options[:except] then self.class.column_names - vestal_journals_options[:except]
else self.class.column_names
end - %w(created_at updated_at)
end
# Returns the activity type. Should be overridden in the journalized class to offer
# multiple types
def activity_type
self.class.name.underscore.pluralize
end
# Specifies the attributes used during journal creation. This is separated into its own
# method so that it can be overridden by the VestalVersions::Users feature.
def journal_attributes
attributes = { :journaled_id => self.id, :activity_type => activity_type,
:changes => journal_changes, :version => last_version + 1,
:notes => journal_notes, :user_id => (journal_user.try(:id) || User.current) }
end
end
end
end
\ No newline at end of file
# This file is part of the acts_as_journalized plugin for the redMine
# project management software
#
# Copyright (C) 2010 Finn GmbH, http://finn.de
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# These hooks make sure journals are properly created and updated with Redmine user detail,
# notes and associated custom fields
module Redmine::Acts::Journalized
module Deprecated
# Old mailer API
def recipients
notified = project.notified_users
notified.reject! {|user| !visible?(user)}
notified.collect(&:mail)
end
def current_journal
last_journal
end
# FIXME: When the new API is settled, remove me
Redmine::Acts::Event::InstanceMethods.instance_methods(false).each do |m|
if m.start_with? "event_"
class_eval(<<-RUBY, __FILE__, __LINE__)
def #{m}
if last_journal.nil?
begin
journals << self.class.journal_class.create(journal_attributes)
reset_journal_changes
reset_journal
true
rescue Exception => e # FIXME: What to do? This likely means that the parent record is invalid!
p e
p e.message
p e.backtrace
false
end
journals.reload
end
return last_journal.#{m}
end
RUBY
end
end
def event_url(options = {})
last_journal.event_url(options)
end
# deprecate :recipients => "use #last_journal.recipients"
# deprecate :current_journal => "use #last_journal"
end
end
# redMine - project management software # This file is part of the acts_as_journalized plugin for the redMine
# Copyright (C) 2006-2008 Jean-Philippe Lang # project management software
#
# Copyright (C) 2010 Finn GmbH, http://finn.de
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2 # as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version. # of the License, or (at your option) any later version.
# #
# This program is distributed in the hope that it will be useful, # This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details. # GNU General Public License for more details.
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software # along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module JournalsHelper module Redmine::Acts::Journalized
def render_notes(issue, journal, options={}) module FormatHooks
content = '' def self.included(base)
editable = User.current.logged? && (User.current.allowed_to?(:edit_issue_notes, issue.project) || (journal.user == User.current && User.current.allowed_to?(:edit_own_issue_notes, issue.project))) base.extend ClassMethods
links = [] end
if !journal.notes.blank?
links << link_to_remote(image_tag('comment.png'), module ClassMethods
{ :url => {:controller => 'journals', :action => 'new', :id => issue, :journal_id => journal} }, # Shortcut to register a formatter for a number of fields
:title => l(:button_quote)) if options[:reply_links] def register_on_journal_formatter(formatter, *field_names)
links << link_to_in_place_notes_editor(image_tag('edit.png'), "journal-#{journal.id}-notes", formatter = formatter.to_sym
{ :controller => 'journals', :action => 'edit', :id => journal }, field_names.collect(&:to_s).each do |field|
:title => l(:button_edit)) if editable JournalFormatter.register :class => self.journal_class.name.to_sym, field => formatter
end
end
# Shortcut to register a new proc as a named formatter. Overwrites
# existing formatters with the same name
def register_journal_formatter(formatter)
JournalFormatter.register formatter.to_sym => Proc.new
end
end end
content << content_tag('div', links.join(' '), :class => 'contextual') unless links.empty?
content << textilizable(journal, :notes)
css_classes = "wiki"
css_classes << " editable" if editable
content_tag('div', content, :id => "journal-#{journal.id}-notes", :class => css_classes)
end
def link_to_in_place_notes_editor(text, field_id, url, options={})
onclick = "new Ajax.Request('#{url_for(url)}', {asynchronous:true, evalScripts:true, method:'get'}); return false;"
link_to text, '#', options.merge(:onclick => onclick)
end end
end end
# This file included as part of the acts_as_journalized plugin for
# the redMine project management software; You can redistribute it
# and/or modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# The original copyright and license conditions are:
# Copyright (c) 2009 Steve Richert
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
module Redmine::Acts::Journalized
# Provides +journaled+ options conjournal and cleanup.
module Options
def self.included(base) # :nodoc:
base.class_eval do
extend ClassMethods
end
end
# Class methods that provide preparation of options passed to the +journaled+ method.
module ClassMethods
# The +prepare_journaled_options+ method has three purposes:
# 1. Populate the provided options with default values where needed
# 2. Prepare options for use with the +has_many+ association
# 3. Save user-configurable options in a class-level variable
#
# Options are given priority in the following order:
# 1. Those passed directly to the +journaled+ method
# 2. Those specified in an initializer +configure+ block
# 3. Default values specified in +prepare_journaled_options+
#
# The method is overridden in feature modules that require specific options outside the
# standard +has_many+ associations.
def prepare_journaled_options(options)
options.symbolize_keys!
options.reverse_merge!(Configuration.options)
options.reverse_merge!(
:class_name => 'Journal',
:dependent => :delete_all
)
options.reverse_merge!(
:order => "#{options[:class_name].constantize.table_name}.version ASC"
)
class_inheritable_accessor :vestal_journals_options
self.vestal_journals_options = options.dup
options.merge!(
:extend => Array(options[:extend]).unshift(Versions)
)
end
end
end
end
# Redmine - project management software # This file is part of the acts_as_journalized plugin for the redMine
# Copyright (C) 2006-2011 Jean-Philippe Lang # project management software
#
# Copyright (C) 2010 Finn GmbH, http://finn.de
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2 # as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version. # of the License, or (at your option) any later version.
# #
# This program is distributed in the hope that it will be useful, # This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details. # GNU General Public License for more details.
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software # along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class JournalDetail < ActiveRecord::Base module Redmine::Acts::Journalized
belongs_to :journal module Permissions
# Default implementation of journal editing permission
# Is overridden if defined in the journalized model directly
def journal_editable_by?(user)
return true if user.admin?
if respond_to? :editable_by?
editable_by? user
else
permission = :"edit_#{self.class.to_s.pluralize.downcase}"
p = @project || (project if respond_to? :project)
options = { :global => p.present? }
user.allowed_to? permission, p, options
end
end
end
end end
# This file included as part of the acts_as_journalized plugin for
# the redMine project management software; You can redistribute it
# and/or modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# The original copyright and license conditions are:
# Copyright (c) 2009 Steve Richert
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
module Redmine::Acts::Journalized
# Ties into the existing ActiveRecord::Base#reload method to ensure that journal information
# is properly reset.
module Reload
def self.included(base) # :nodoc:
base.class_eval do
include InstanceMethods
alias_method_chain :reload, :journals
end
end
# Adds instance methods into ActiveRecord::Base to tap into the +reload+ method.
module InstanceMethods
# Overrides ActiveRecord::Base#reload, resetting the instance-variable-cached journal number
# before performing the original +reload+ method.
def reload_with_journals(*args)
reset_journal
reload_without_journals(*args)
end
end
end
end
# This file included as part of the acts_as_journalized plugin for
# the redMine project management software; You can redistribute it
# and/or modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# The original copyright and license conditions are:
# Copyright (c) 2009 Steve Richert
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
module Redmine::Acts::Journalized
# Adds the ability to "reset" (or hard revert) a journaled ActiveRecord::Base instance.
module Reset
def self.included(base) # :nodoc:
base.class_eval do
include InstanceMethods
end
end
# Adds the instance methods required to reset an object to a previous journal.
module InstanceMethods
# Similar to +revert_to!+, the +reset_to!+ method reverts an object to a previous journal,
# only instead of creating a new record in the journal history, +reset_to!+ deletes all of
# the journal history that occurs after the journal reverted to.
#
# The action taken on each journal record after the point of rejournal is determined by the
# <tt>:dependent</tt> option given to the +journaled+ method. See the +journaled+ method
# documentation for more details.
def reset_to!(value)
if saved = skip_journal{ revert_to!(value) }
journals.send(:delete_records, journals.after(value))
reset_journal
end
saved
end
end
end
end
# This file included as part of the acts_as_journalized plugin for
# the redMine project management software; You can redistribute it
# and/or modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# The original copyright and license conditions are:
# Copyright (c) 2009 Steve Richert
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
module Redmine::Acts::Journalized
# Enables versioned ActiveRecord::Base instances to revert to a previously saved version.
module Reversion
def self.included(base) # :nodoc:
base.class_eval do
include InstanceMethods
end
end
# Provides the base instance methods required to revert a journaled instance.
module InstanceMethods
# Returns the current version number for the versioned object.
def version
@version ||= last_version
end
def last_journal
journals.last
end
# Accepts a value corresponding to a specific journal record, builds a history of changes
# between that journal and the current journal, and then iterates over that history updating
# the object's attributes until the it's reverted to its prior state.
#
# The single argument should adhere to one of the formats as documented in the +at+ method of
# VestalVersions::Versions.
#
# After the object is reverted to the target journal, it is not saved. In order to save the
# object after the rejournal, use the +revert_to!+ method.
#
# The journal number of the object will reflect whatever journal has been reverted to, and
# the return value of the +revert_to+ method is also the target journal number.
def revert_to(value)
to_number = journals.journal_at(value)
changes_between(journal, to_number).each do |attribute, change|
write_attribute(attribute, change.last)
end
reset_journal(to_number)
end
# Behaves similarly to the +revert_to+ method except that it automatically saves the record
# after the rejournal. The return value is the success of the save.
def revert_to!(value)
revert_to(value)
reset_journal if saved = save
saved
end
# Returns a boolean specifying whether the object has been reverted to a previous journal or
# if the object represents the latest journal in the journal history.
def reverted?
version != last_version
end
private
# Returns the number of the last created journal in the object's journal history.
#
# If no associated journals exist, the object is considered at version 0.
def last_version
@last_version ||= journals.maximum(:version) || 0
end
# Clears the cached version number instance variables so that they can be recalculated.
# Useful after a new version is created.
def reset_journal(version = nil)
@last_version = nil if version.nil?
@version = version
end
end
end
end
\ No newline at end of file
# This file is part of the acts_as_journalized plugin for the redMine
# project management software
#
# Copyright (C) 2010 Finn GmbH, http://finn.de
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# These hooks make sure journals are properly created and updated with Redmine user detail,
# notes and associated custom fields
module Redmine::Acts::Journalized
module SaveHooks
def self.included(base)
base.extend ClassMethods
base.class_eval do
before_save :init_journal
after_save :reset_instance_variables
attr_reader :journal_notes, :journal_user
end
end
# Saves the current custom values, notes and journal to include them in the next journal
# Called before save
def init_journal(user = User.current, notes = "")
@journal_notes ||= notes
@journal_user ||= user
@associations_before_save ||= {}
@associations = {}
save_possible_association :custom_values, :key => :custom_field_id, :value => :value
save_possible_association :attachments, :key => :id, :value => :filename
@current_journal ||= last_journal
end
# Saves the notes and custom value changes in the last Journal
# Called before create_journal
def update_journal
unless (@associations || {}).empty?
changed_associations = {}
changed_associations.merge!(possibly_updated_association :custom_values)
changed_associations.merge!(possibly_updated_association :attachments)
end
unless changed_associations.blank?
update_extended_journal_contents(changed_associations)
end
end
def reset_instance_variables
if last_journal != @current_journal
if last_journal.user != @journal_user
last_journal.update_attribute(:user_id, @journal_user.id)
end
end
@associations_before_save = @current_journal = @journal_notes = @journal_user = nil
end
def save_possible_association(method, options)
@associations[method] = options
if self.respond_to? method
@associations_before_save[method] ||= send(method).inject({}) do |hash, cv|
hash[cv.send(options[:key])] = cv.send(options[:value])
hash
end
end
end
def possibly_updated_association(method)
if @associations_before_save[method]
# Has custom values from init_journal_notes
return changed_associations(method, @associations_before_save[method])
end
{}
end
# Saves the notes and changed custom values to the journal
# Creates a new journal, if no immediate attributes were changed
def update_extended_journal_contents(changed_associations)
journal_changes.merge!(changed_associations)
end
def changed_associations(method, previous)
send(method).reload # Make sure the associations are reloaded
send(method).inject({}) do |hash, c|
key = c.send(@associations[method][:key])
new_value = c.send(@associations[method][:value])
if previous[key].blank? && new_value.blank?
# The key was empty before, don't add a blank value
elsif previous[key] != new_value
# The key's value changed
hash["#{method}#{key}"] = [previous[key], new_value]
end
hash
end
end
module ClassMethods
end
end
end
# This file included as part of the acts_as_journalized plugin for
# the redMine project management software; You can redistribute it
# and/or modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# The original copyright and license conditions are:
# Copyright (c) 2009 Steve Richert
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
module Redmine::Acts::Journalized
# Provides a way for information to be associated with specific journals as to who was
# responsible for the associated update to the parent.
module Users
def self.included(base) # :nodoc:
Journal.send(:include, JournalMethods)
base.class_eval do
include InstanceMethods
attr_accessor :updated_by
alias_method_chain :journal_attributes, :user
end
end
# Methods added to journaled ActiveRecord::Base instances to enable journaling with additional
# user information.
module InstanceMethods
private
# Overrides the +journal_attributes+ method to include user information passed into the
# parent object, by way of a +updated_by+ attr_accessor.
def journal_attributes_with_user
journal_attributes_without_user.merge(:user => updated_by || User.current)
end
end
# Instance methods added to Redmine::Acts::Journalized::Journal to accomodate incoming
# user information.
module JournalMethods
def self.included(base) # :nodoc:
base.class_eval do
belongs_to :user
alias_method_chain :user=, :name
end
end
# Overrides the +user=+ method created by the polymorphic +belongs_to+ user association.
# Based on the class of the object given, either the +user+ association columns or the
# +user_name+ string column is populated.
def user_with_name=(value)
case value
when ActiveRecord::Base then self.user_without_name = value
else self.user = User.find_by_login(value)
end
end
end
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment