Merge branch 'stable' into unstable

pull/351/head
Eric Davis 14 years ago
commit ddeb1a2a0f
  1. 3
      Gemfile
  2. 2
      Rakefile
  3. 2
      app/controllers/issues_controller.rb
  4. 1
      app/models/changeset.rb
  5. 21
      app/models/issue.rb
  6. 2
      app/models/journal.rb
  7. 2
      app/models/journal_observer.rb
  8. 2
      app/models/mail_handler.rb
  9. 4
      app/models/setting.rb
  10. 2
      app/models/wiki_content.rb
  11. 8
      app/views/auth_sources/index.html.erb
  12. 6
      app/views/common/_diff.rhtml
  13. 2
      app/views/documents/_form.rhtml
  14. 5
      app/views/journals/index.rxml
  15. 3
      app/views/users/show.rhtml
  16. 1
      app/views/wiki/show.rhtml
  17. 2
      config/environment.rb
  18. 4
      config/locales/en.yml
  19. 22
      config/locales/pt-BR.yml
  20. 122
      db/migrate/20100714111651_generalize_journals.rb
  21. 55
      db/migrate/20100714111651_prepare_journals_for_acts_as_journalized.rb
  22. 47
      db/migrate/20100714111652_update_journals_for_acts_as_journalized.rb
  23. 80
      db/migrate/20100714111653_build_initial_journals_for_acts_as_journalized.rb
  24. 35
      db/migrate/20100714111654_add_changes_from_journal_details_for_acts_as_journalized.rb
  25. 2
      db/migrate/20100804112053_merge_wiki_versions_with_journals.rb
  26. 34
      doc/CHANGELOG.rdoc
  27. 1
      extra/svn/Redmine.pm
  28. 2
      lib/redmine/version.rb
  29. 2
      lib/tasks/documentation.rake
  30. 37
      lib/tasks/release.rake
  31. 8
      public/javascripts/controls.js
  32. 15
      public/javascripts/dragdrop.js
  33. 19
      public/javascripts/effects.js
  34. 5129
      public/javascripts/prototype.js
  35. 74
      test/fixtures/mail_handler/ticket_reply_by_message_id.eml
  36. 62
      test/unit/issue_nested_set_test.rb
  37. 22
      test/unit/issue_test.rb
  38. 124
      test/unit/journal_observer_test.rb
  39. 38
      test/unit/journal_test.rb
  40. 15
      test/unit/mail_handler_test.rb
  41. 7
      vendor/plugins/acts_as_journalized/lib/acts_as_journalized.rb
  42. 2
      vendor/plugins/acts_as_journalized/lib/redmine/acts/journalized/changes.rb

@ -1,10 +1,11 @@
source :rubygems
gem "rails", "2.3.11"
gem "rails", "2.3.12"
gem "coderay", "~> 0.9.7"
gem "i18n", "~> 0.4.2"
gem "rubytree", "~> 0.5.2", :require => 'tree'
gem "rdoc", ">= 2.4.2"
group :test do
gem 'shoulda', '~> 2.10.3'

@ -5,6 +5,6 @@ require(File.join(File.dirname(__FILE__), 'config', 'boot'))
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
require 'rdoc/task'
require 'tasks/rails'

@ -27,9 +27,7 @@ class IssuesController < ApplicationController
rescue_from Query::StatementInvalid, :with => :query_statement_invalid
helper :journals
include JournalsHelper
helper :projects
include ProjectsHelper
include CustomFieldsHelper
include IssueRelationsHelper

@ -23,6 +23,7 @@ class Changeset < ActiveRecord::Base
:event_description => :long_comments,
:event_datetime => :committed_on,
:event_url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.identifier}},
:event_author => Proc.new {|o| o.author},
:activity_timestamp => "#{table_name}.committed_on",
:activity_find_options => {:include => [:user, {:repository => :project}]}
acts_as_searchable :columns => 'comments',

@ -280,6 +280,13 @@ class Issue < ActiveRecord::Base
end
end
# Bug #501: browsers might swap the line endings causing a Journal.
if attrs.has_key?('description') && attrs['description'].present?
if attrs['description'].gsub(/\r\n?/,"\n") == self.description
attrs.delete('description')
end
end
self.attributes = attrs
end
@ -529,6 +536,20 @@ class Issue < ActiveRecord::Base
"#{tracker} ##{id}: #{subject}"
end
# The number of "items" this issue spans in it's nested set
#
# A parent issue would span all of it's children + 1 left + 1 right (3)
#
# | parent |
# || child ||
#
# A child would span only itself (1)
#
# |child|
def nested_set_span
rgt - lft
end
# Returns a string of css classes that apply to the issue
def css_classes
s = "issue status-#{status.position} priority-#{priority.position}"

@ -23,7 +23,7 @@ class Journal < ActiveRecord::Base
# Make sure each journaled model instance only has unique version ids
validates_uniqueness_of :version, :scope => [:journaled_id, :type]
belongs_to :journaled
belongs_to :journaled, :touch => true
belongs_to :user
# ActiveRecord::Base#changes is an existing method, so before serializing the +changes+ column,

@ -15,7 +15,7 @@ class JournalObserver < ActiveRecord::Observer
attr_accessor :send_notification
def after_create(journal)
if journal.type == "IssueJournal" and journal.version > 1 and self.send_notification
if journal.type == "IssueJournal" and !journal.initial? and send_notification
after_create_issue_journal(journal)
end
clear_notification

@ -165,7 +165,7 @@ class MailHandler < ActionMailer::Base
end
# Reply will be added to the issue
def receive_journal_reply(journal_id)
def receive_issue_journal_reply(journal_id)
journal = Journal.find_by_id(journal_id)
if journal and journal.journaled.is_a? Issue
receive_issue_reply(journal.journaled_id)

@ -96,7 +96,7 @@ class Setting < ActiveRecord::Base
# Returns the value of the setting named name
def self.[](name)
Marshal.load(Rails.cache.fetch("chiliproject/setting/#{name}") {Marshal.dump(find_or_default(name).value)}).freeze
Marshal.load(Rails.cache.fetch("chiliproject/setting/#{name}") {Marshal.dump(find_or_default(name).value)})
end
def self.[]=(name, v)
@ -104,7 +104,7 @@ class Setting < ActiveRecord::Base
setting.value = (v ? v : "")
Rails.cache.delete "chiliproject/setting/#{name}"
setting.save
setting.value.freeze
setting.value
end
# Defines getter and setter for each setting

@ -25,7 +25,7 @@ class WikiContent < ActiveRecord::Base
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}},
:event_url => Proc.new {|o| {:controller => 'wiki', :action => 'show', :id => o.page.title, :project_id => o.page.wiki.project, :version => o.version}},
:activity_type => 'wiki_edits',
:activity_permission => :view_wiki_edits,
:activity_find_options => { :include => { :page => { :wiki => :project } } }

@ -15,10 +15,10 @@
<tbody>
<% for source in @auth_sources %>
<tr class="<%= cycle("odd", "even") %>">
<td><%= link_to source.name, :action => 'edit', :id => source%></td>
<td align="center"><%= source.auth_method_name %></td>
<td align="center"><%= source.host %></td>
<td align="center"><%= source.users.count %></td>
<td><%= link_to(h(source.name), :action => 'edit', :id => source)%></td>
<td align="center"><%= h source.auth_method_name %></td>
<td align="center"><%= h source.host %></td>
<td align="center"><%= h source.users.count %></td>
<td class="buttons">
<%= link_to l(:button_test), :action => 'test_connection', :id => source %>
<%= link_to l(:button_delete), { :action => 'destroy', :id => source },

@ -5,7 +5,7 @@
<% if diff.diff_type == 'sbs' -%>
<table class="filecontent">
<thead>
<tr><th colspan="4" class="filename"><%=to_utf8 table_file.file_name %></th></tr>
<tr><th colspan="4" class="filename"><%=to_utf8_for_attachments table_file.file_name %></th></tr>
</thead>
<tbody>
<% table_file.each_line do |spacing, line| -%>
@ -17,11 +17,11 @@
<tr>
<th class="line-num"><%= line.nb_line_left %></th>
<td class="line-code <%= line.type_diff_left %>">
<pre><%=to_utf8 line.html_line_left %></pre>
<pre><%=to_utf8_for_attachments line.html_line_left %></pre>
</td>
<th class="line-num"><%= line.nb_line_right %></th>
<td class="line-code <%= line.type_diff_right %>">
<pre><%=to_utf8 line.html_line_right %></pre>
<pre><%=to_utf8_for_attachments line.html_line_right %></pre>
</td>
</tr>
<% end -%>

@ -1,7 +1,7 @@
<%= error_messages_for 'document' %>
<div class="box">
<!--[form:document]-->
<p><label for="document_category_id"><%=l(:field_category)%></label>
<p><label for="document_category_id"><%=l(:field_category)%> <span class="required">*</span></label>
<%= select('document', 'category_id', DocumentCategory.all.collect {|c| [c.name, c.id]}) %></p>
<p><label for="document_title"><%=l(:field_title)%> <span class="required">*</span></label>

@ -19,8 +19,9 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do
end
xml.content "type" => "html" do
xml.text! '<ul>'
change.details.each do |detail|
xml.text! '<li>' + change.render_detail(detail, false) + '</li>'
change.changes.each do |detail|
change_content = change.render_detail(detail, false)
xml.text!(content_tag(:li, change_content)) if change_content.present?
end
xml.text! '</ul>'
xml.text! textilizable(change, :notes, :only_path => false) unless change.notes.blank?

@ -5,6 +5,7 @@
<h2><%= avatar @user, :size => "50" %> <%=h @user.name %></h2>
<div class="splitcontentleft">
<%= call_hook :view_account_left_top, :user => @user %>
<ul>
<% unless @user.pref.hide_mail %>
<li><%=l(:field_mail)%>: <%= mail_to(h(escape_javascript(@user.mail)), nil, :encode => 'javascript') %></li>
@ -20,6 +21,8 @@
<% end %>
</ul>
<%= call_hook :view_account_left_middle, :user => @user %>
<% unless @memberships.empty? %>
<h3><%=l(:label_project_plural)%></h3>
<ul>

@ -49,6 +49,7 @@
<%= f.link_to 'Atom', :url => {:controller => 'activities', :action => 'index', :id => @project, :show_wiki_edits => 1, :key => User.current.rss_key} %>
<%= f.link_to 'HTML', :url => {:id => @page.title, :version => @content.version} %>
<%= f.link_to 'TXT', :url => {:id => @page.title, :version => @content.version} %>
<%= call_hook(:view_wiki_show_other_formats, {:link_builder => f, :url_params => {:id => @page.title, :version => @content.version}}) %>
<% end if User.current.allowed_to?(:export_wiki_pages, @project) %>
<% content_for :header_tags do %>

@ -18,7 +18,7 @@
# ENV['RAILS_ENV'] ||= 'production'
# Specifies gem version of Rails to use when vendor/rails is not present
RAILS_GEM_VERSION = '2.3.11' unless defined? RAILS_GEM_VERSION
RAILS_GEM_VERSION = '2.3.12' unless defined? RAILS_GEM_VERSION
# Bootstrap the Rails environment, frameworks, and default configuration
require File.join(File.dirname(__FILE__), 'boot')

@ -931,8 +931,8 @@ en:
text_powered_by: "Powered by %{link}"
text_warn_on_leaving_unsaved: "The current page contains unsaved text that will be lost if you leave this page."
text_default_encoding: "Default: UTF-8"
text_mercurial_repo_example: "local repository (e.g. /hgrepo, c:\hgrepo)"
text_git_repo_example: "a bare and local repository (e.g. /gitrepo, c:\gitrepo)"
text_mercurial_repo_example: "local repository (e.g. /hgrepo, c:\\hgrepo)"
text_git_repo_example: "a bare and local repository (e.g. /gitrepo, c:\\gitrepo)"
default_role_manager: Manager
default_role_developer: Developer

@ -968,18 +968,18 @@ pt-BR:
label_my_queries: Minhas consultas personalizadas
text_journal_changed_no_detail: "%{label} atualizado(a)"
label_news_comment_added: Notícia recebeu um comentário
button_expand_all: Expand all
button_collapse_all: Collapse all
label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee
label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author
field_effective_date: Due date
button_expand_all: Expandir tudo
button_collapse_all: Recolher tudo
label_additional_workflow_transitions_for_assignee: Transições adicionais permitidas quando o usuário é o responsável pela tarefa
label_additional_workflow_transitions_for_author: Transições adicionais permitidas quando o usuário é o autor
field_effective_date: Data prevista
label_cvs_path: CVSROOT
text_powered_by: Powered by %{link}
text_default_encoding: "Default: UTF-8"
text_git_repo_example: a bare and local repository (e.g. /gitrepo, c:\gitrepo)
label_notify_member_plural: Email issue updates
label_path_encoding: Path encoding
text_mercurial_repo_example: local repository (e.g. /hgrepo, c:\hgrepo)
text_powered_by: Tecnologia empregada por %{link}
text_default_encoding: "Padrão: UTF-8"
text_git_repo_example: "um repositório local do tipo bare (ex.: /gitrepo, c:\\gitrepo)"
label_notify_member_plural: Enviar atualizações da tarefa por e-mail
label_path_encoding: Codificação do caminho
text_mercurial_repo_example: "repositório local (ex.: /hgrepo, c:\\hgrepo)"
label_cvs_module: Módulo
label_filesystem_path: Diretório raiz
label_darcs_path: Diretório raiz

@ -1,122 +0,0 @@
#-- copyright
# ChiliProject is a project management system.
#
# Copyright (C) 2010-2011 the ChiliProject Team
#
# 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.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
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

@ -0,0 +1,55 @@
#-- copyright
# ChiliProject is a project management system.
#
# Copyright (C) 2010-2011 the ChiliProject Team
#
# 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.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
class PrepareJournalsForActsAsJournalized < 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
end
def self.down
change_table "journals" do |t|
t.rename :journaled_id, :journalized_id
t.rename :created_at, :created_on
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
end
end

@ -0,0 +1,47 @@
Redmine::Activity.providers.values.flatten.uniq.collect(&:underscore).each {|klass| require_dependency klass }
class UpdateJournalsForActsAsJournalized < 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
say_with_time("Updating existing Journals...") do
Journal.all.group_by(&:journaled_id).each_pair do |id, journals|
journals.sort_by(&:created_at).each_with_index do |j, idx|
# Recast the basic Journal into it's STI journalized class so callbacks work (#467)
klass_name = "#{j.journalized_type}Journal"
j = j.becomes(klass_name.constantize)
j.type = klass_name
j.version = idx + 2 # initial journal should be 1
j.activity_type = j.journalized_type.constantize.activity_provider_options.keys.first
begin
j.save(false)
rescue ActiveRecord::RecordInvalid => ex
puts "Error saving: #{j.class.to_s}##{j.id} - #{ex.message}"
end
end
end
end
change_table :journals do |t|
t.remove :journalized_type
end
end
def self.down
change_table "journals" do |t|
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
end
end

@ -0,0 +1,80 @@
class BuildInitialJournalsForActsAsJournalized < 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
# Reset class and subclasses, otherwise they will try to save using older attributes
Journal.reset_column_information
Journal.send(:subclasses).each do |klass|
klass.reset_column_information if klass.respond_to?(:reset_column_information)
end
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
say_with_time("Building initial journals for #{p.class_name}") do
activity_type = p.activity_provider_options.keys.first
p.find(:all).each do |o|
# Create initial journals
new_journal = o.journals.build
# Mock up a list of changes for the creation journal based on Class defaults
new_attributes = o.class.new.attributes.except(o.class.primary_key,
o.class.inheritance_column,
:updated_on,
:updated_at,
:lock_version,
:lft,
:rgt)
creation_changes = {}
new_attributes.each do |name, default_value|
# Set changes based on the initial value to current. Can't get creation value without
# rebuiling the object history
creation_changes[name] = [default_value, o.send(name)] # [initial_value, creation_value]
end
new_journal.changes = creation_changes
new_journal.version = 1
new_journal.activity_type = activity_type
if o.respond_to?(:author)
new_journal.user = o.author
elsif o.respond_to?(:user)
new_journal.user = o.user
end
# Using rescue and save! here because either the Journal or the
# touched record could fail. This will catch either error and continue
begin
new_journal.save!
new_journal.reload
# Backdate journal
if o.respond_to?(:created_at)
new_journal.update_attribute(:created_at, o.created_at)
elsif o.respond_to?(:created_on)
new_journal.update_attribute(:created_at, o.created_on)
end
rescue ActiveRecord::RecordInvalid => ex
if new_journal.errors.count == 1 && new_journal.errors.first[0] == "version"
# Skip, only error was from creating the initial journal for a record that already had one.
else
puts "ERROR: errors creating the initial journal for #{o.class.to_s}##{o.id.to_s}:"
puts " #{ex.message}"
end
end
end
end
end
end
def self.down
# No-op
end
end

@ -0,0 +1,35 @@
class AddChangesFromJournalDetailsForActsAsJournalized < 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
say_with_time("Adding changes from JournalDetails") do
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
begin
journal.update_attribute(:changes, changes.to_yaml)
rescue ActiveRecord::RecordInvalid => ex
puts "Error saving: #{journal.class.to_s}##{journal.id} - #{ex.message}"
end
end
end
end
def self.down
# No-op
end
end

@ -20,7 +20,7 @@ class MergeWikiVersionsWithJournals < ActiveRecord::Migration
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")
:notes => wv.comments, :created_at => wv.updated_on, :activity_type => "wiki_edits")
changes = {}
changes["compression"] = wv.compression
changes["data"] = wv.data

@ -7,9 +7,11 @@
* Bug #343: Review Gantt and Calender links from 07cf681
* Bug #345: Entering large numbers for 'Estimated Time' fails with 'Invalid big Decimal Value'
* Bug #346: I18n YAML files not parsable with psych yaml library
* Bug #383: Fix broken tests in unstable
* Bug #383: Fix broken tests in unstable caused by conflicting to_utf8 method names
* Bug #389: Context menu doesn't work in Opera
* Bug #390: mysql2 incompatibility in WikiPage model
* Bug #397: FIXME in generalize_journals migration
* Bug #398: Remove helper calls from IssuesController
* Bug #400: Review and fix the Activity event types
* Bug #401: Move JournalsHelpers from aaj to the core
* Bug #403: [AAJ] Attachment has it's files and documents activity provider removed but only documents added
@ -27,17 +29,42 @@
* Bug #419: Issue list context menu not working in IE9
* Bug #422: cvs test are not working
* Bug #423: Remove explicit render from WikiController#show
* Bug #437: Encoding error on Ruby 1.9 in pdf exports
* Bug #441: Creating a Journal does not update the journaled record's updated_at/on attribute
* Bug #442: Issue atom feed shows "issue creation" journal, didn't before
* Bug #443: IssuesControllerTest.test_show_atom test failure on 1.9.2
* Bug #444: ChangesetTest and RepositoryGitTest test failures on 1.9.2
* Bug #445: Track initial attributes in a Journal when created
* Bug #453: Update to Rails 2.3.12 to fix some bugs
* Bug #466: SVN: Apache initialization error
* Bug #467: uninitialized constant Journal::Journaled
* Bug #468: Lost WIKI history timestamps during 2.0.0rc1 upgrade.
* Bug #469: Wong URL for WIKI activity entries in 2.0.0rc2
* Bug #474: Changesets are displaying the wrong user and commit date in the Activity
* Bug #475: News, docs, changesets and time activities were not migrated to 2.0.0rc2
* Bug #477: Getting rid of "rake/rdoctask is deprecated." warning
* Bug #479: Generalize Journals migrations does too much
* Bug #480: Issue Journal replies get ignored
* Bug #493: uninitialized constant TimeEntryJournal
* Bug #501: Updating a ticket that was created by email forces a "change" of description
* Bug #503: 2.0.0RC3 - YAML Parser fails in ruby 1.9
* Feature #112: Provide a library function to detect the database type used
* Feature #123: Review and Merge acts_as_journalized
* Feature #196: Upgrade to Rails 2.3-latest
* Feature #197: Rake task to manage copyright inside of source files
* Feature #216: Remove the rubygems hack from boot.rb
* Feature #217: Remove the hack to require a specific i18n version in boot.rb
* Feature #269: Refactor lib/redmine/menu_manager.rb to increase extensibility
* Feature #279: Optional start date on Versions
* Feature #288: Review latest Redmine commits
* Feature #289: Switch to helper :all
* Feature #290: Add bundler
* Feature #310: Option to skip mail notifications on issue updates
* Feature #350: Setting model should use Rails.cache instead of class variable
* Feature #416: Refactor watcher_tag and watcher_link to use css selectors for the replace action
* Feature #436: Clean up trailing whitespace and tabs
* Feature #462: pt-BR translation update
* Feature #473: pt-BR translation fix
* Task #123: Review and Merge acts_as_journalized
* Task #197: Rake task to manage copyright inside of source files
* Task #288: Review latest Redmine commits
@ -59,6 +86,11 @@
** Patch #7598: Extensible MailHandler
** Patch #7795: Internal server error at journals#index with custom fields
== 2011-06-27 v1.5.0
* Bug #490: XSS in app/views/auth_sources/index.html.erb
* Feature #488: Hook for additional formats on Wiki#show page
== 2011-05-27 v1.4.0
* Bug #81: Replace favicon

@ -480,6 +480,7 @@ sub is_member {
sub get_project_identifier {
my $r = shift;
my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
my $location = $r->location;
my ($identifier) = $r->uri =~ m{$location/*([^/]+)};
$identifier =~ s/\.git$// if (defined $cfg->{RedmineGitSmartHttp} and $cfg->{RedmineGitSmartHttp});

@ -32,7 +32,7 @@ module Redmine
#
# 2.0.0debian-2
def self.special
'RC1'
''
end
def self.revision

@ -18,7 +18,7 @@ Rake::Task["doc/app/index.html"].clear
namespace :doc do
desc "Generate documentation for the application. Set custom template with TEMPLATE=/path/to/rdoc/template.rb or title with TITLE=\"Custom Title\""
Rake::RDocTask.new("app") { |rdoc|
RDoc::Task.new("app") { |rdoc|
rdoc.rdoc_dir = 'doc/app'
rdoc.template = ENV['template'] if ENV['template']
rdoc.title = ENV['title'] || "ChiliProject"

@ -0,0 +1,37 @@
#-- copyright
# ChiliProject is a project management system.
#
# Copyright (C) 2010-2011 the ChiliProject Team
#
# 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.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'fileutils'
desc "Package up a ChiliProject release from git. example: `rake release[1.1.0]`"
task :release, [:version] do |task, args|
version = args[:version]
abort "Missing version in the form of 1.0.0" unless version.present?
dir = Pathname.new(ENV['HOME']) + 'dev' + 'chiliproject' + 'packages'
FileUtils.mkdir_p dir
commands = [
"cd #{dir}",
"git clone git://github.com/chiliproject/chiliproject.git chiliproject-#{version}",
"cd chiliproject-#{version}/",
"git checkout v#{version}",
"rm -vRf #{dir}/chiliproject-#{version}/.git",
"cd #{dir}",
"tar -zcvf chiliproject-#{version}.tar.gz chiliproject-#{version}",
"zip -r -9 chiliproject-#{version}.zip chiliproject-#{version}",
"md5sum chiliproject-#{version}.tar.gz chiliproject-#{version}.zip > chiliproject-#{version}.md5sum",
"echo 'Release ready'"
].join(' && ')
system(commands)
end

@ -1,6 +1,8 @@
// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// (c) 2005-2008 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
// (c) 2005-2008 Jon Tirsen (http://www.tirsen.com)
// script.aculo.us controls.js v1.9.0, Thu Dec 23 16:54:48 -0500 2010
// Copyright (c) 2005-2010 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// (c) 2005-2010 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
// (c) 2005-2010 Jon Tirsen (http://www.tirsen.com)
// Contributors:
// Richard Livsey
// Rahul Bhargava

@ -1,5 +1,6 @@
// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// (c) 2005-2008 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz)
// script.aculo.us dragdrop.js v1.9.0, Thu Dec 23 16:54:48 -0500 2010
// Copyright (c) 2005-2010 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//
// script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/
@ -311,7 +312,7 @@ var Draggable = Class.create({
tag_name=='TEXTAREA')) return;
var pointer = [Event.pointerX(event), Event.pointerY(event)];
var pos = Position.cumulativeOffset(this.element);
var pos = this.element.cumulativeOffset();
this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });
Draggables.activate(this);
@ -373,7 +374,7 @@ var Draggable = Class.create({
if (this.options.scroll == window) {
with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; }
} else {
p = Position.page(this.options.scroll);
p = Position.page(this.options.scroll).toArray();
p[0] += this.options.scroll.scrollLeft + Position.deltaX;
p[1] += this.options.scroll.scrollTop + Position.deltaY;
p.push(p[0]+this.options.scroll.offsetWidth);
@ -454,7 +455,7 @@ var Draggable = Class.create({
},
draw: function(point) {
var pos = Position.cumulativeOffset(this.element);
var pos = this.element.cumulativeOffset();
if(this.options.ghosting) {
var r = Position.realOffset(this.element);
pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY;
@ -730,7 +731,7 @@ var Sortable = {
}
// keep reference
this.sortables[element.id] = options;
this.sortables[element.identify()] = options;
// for onupdate
Draggables.addObserver(new SortableObserver(element, options.onUpdate));
@ -825,7 +826,7 @@ var Sortable = {
hide().addClassName('dropmarker').setStyle({position:'absolute'});
document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
}
var offsets = Position.cumulativeOffset(dropon);
var offsets = dropon.cumulativeOffset();
Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'});
if(position=='after')

@ -1,4 +1,6 @@
// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// script.aculo.us effects.js v1.9.0, Thu Dec 23 16:54:48 -0500 2010
// Copyright (c) 2005-2010 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// Contributors:
// Justin Palmer (http://encytemedia.com/)
// Mark Pilgrim (http://diveintomark.org/)
@ -145,14 +147,13 @@ var Effect = {
'blind': ['BlindDown','BlindUp'],
'appear': ['Appear','Fade']
},
toggle: function(element, effect) {
toggle: function(element, effect, options) {
element = $(element);
effect = (effect || 'appear').toLowerCase();
var options = Object.extend({
return Effect[ Effect.PAIRS[ effect ][ element.visible() ? 1 : 0 ] ](element, Object.extend({
queue: { position:'end', scope:(element.id || 'global'), limit: 1 }
}, arguments[2] || { });
Effect[element.visible() ?
Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options);
}, options || {}));
}
};
@ -228,12 +229,6 @@ Effect.Queue = Effect.Queues.get('global');
Effect.Base = Class.create({
position: null,
start: function(options) {
function codeForEvent(options,eventName){
return (
(options[eventName+'Internal'] ? 'this.options.'+eventName+'Internal(this);' : '') +
(options[eventName] ? 'this.options.'+eventName+'(this);' : '')
);
}
if (options && options.transition === false) options.transition = Effect.Transitions.linear;
this.options = Object.extend(Object.extend({ },Effect.DefaultOptions), options || { });
this.currentFrame = 0;

File diff suppressed because it is too large Load Diff

@ -0,0 +1,74 @@
Return-Path: <jsmith@somenet.foo>
Received: from osiris ([127.0.0.1])
by OSIRIS
with hMailServer ; Sat, 21 Jun 2008 18:41:39 +0200
Message-ID: <006a01c8d3bd$ad9baec0$0a00a8c0@osiris>
In-Reply-To: <chiliproject.issue_journal-3.20110617112550@example.net>
From: "John Smith" <jsmith@somenet.foo>
To: <redmine@somenet.foo>
References: <485d0ad366c88_d7014663a025f@osiris.tmail>
Subject: Re: Add ingredients categories
Date: Sat, 21 Jun 2008 18:41:39 +0200
MIME-Version: 1.0
Content-Type: multipart/alternative;
boundary="----=_NextPart_000_0067_01C8D3CE.711F9CC0"
X-Priority: 3
X-MSMail-Priority: Normal
X-Mailer: Microsoft Outlook Express 6.00.2900.2869
X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869
This is a multi-part message in MIME format.
------=_NextPart_000_0067_01C8D3CE.711F9CC0
Content-Type: text/plain;
charset="utf-8"
Content-Transfer-Encoding: quoted-printable
This is reply
------=_NextPart_000_0067_01C8D3CE.711F9CC0
Content-Type: text/html;
charset="utf-8"
Content-Transfer-Encoding: quoted-printable
=EF=BB=BF<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<HTML><HEAD>
<META http-equiv=3DContent-Type content=3D"text/html; charset=3Dutf-8">
<STYLE>BODY {
FONT-SIZE: 0.8em; COLOR: #484848; FONT-FAMILY: Verdana, sans-serif
}
BODY H1 {
FONT-SIZE: 1.2em; MARGIN: 0px; FONT-FAMILY: "Trebuchet MS", Verdana, =
sans-serif
}
A {
COLOR: #2a5685
}
A:link {
COLOR: #2a5685
}
A:visited {
COLOR: #2a5685
}
A:hover {
COLOR: #c61a1a
}
A:active {
COLOR: #c61a1a
}
HR {
BORDER-RIGHT: 0px; BORDER-TOP: 0px; BACKGROUND: #ccc; BORDER-LEFT: 0px; =
WIDTH: 100%; BORDER-BOTTOM: 0px; HEIGHT: 1px
}
.footer {
FONT-SIZE: 0.8em; FONT-STYLE: italic
}
</STYLE>
<META content=3D"MSHTML 6.00.2900.2883" name=3DGENERATOR></HEAD>
<BODY bgColor=3D#ffffff>
<DIV><SPAN class=3Dfooter><FONT face=3DArial color=3D#000000 =
size=3D2>This is=20
reply</FONT></DIV></SPAN></BODY></HTML>
------=_NextPart_000_0067_01C8D3CE.711F9CC0--

@ -30,8 +30,10 @@ class IssueNestedSetTest < ActiveSupport::TestCase
issue1.reload
issue2.reload
assert_equal [issue1.id, nil, 1, 2], [issue1.root_id, issue1.parent_id, issue1.lft, issue1.rgt]
assert_equal [issue2.id, nil, 1, 2], [issue2.root_id, issue2.parent_id, issue2.lft, issue2.rgt]
assert_equal issue1.id, issue1.root_id
assert issue1.leaf?
assert_equal issue2.id, issue2.root_id
assert issue2.leaf?
end
def test_create_child_issue
@ -40,8 +42,8 @@ class IssueNestedSetTest < ActiveSupport::TestCase
parent.reload
child.reload
assert_equal [parent.id, nil, 1, 4], [parent.root_id, parent.parent_id, parent.lft, parent.rgt]
assert_equal [parent.id, parent.id, 2, 3], [child.root_id, child.parent_id, child.lft, child.rgt]
assert_equal [parent.id, nil, 3], [parent.root_id, parent.parent_id, parent.rgt - parent.lft]
assert_equal [parent.id, parent.id, 1], [child.root_id, child.parent_id, child.rgt - child.lft]
end
def test_creating_a_child_in_different_project_should_not_validate
@ -62,9 +64,9 @@ class IssueNestedSetTest < ActiveSupport::TestCase
parent1.reload
parent2.reload
assert_equal [parent1.id, 1, 6], [parent1.root_id, parent1.lft, parent1.rgt]
assert_equal [parent1.id, 4, 5], [parent2.root_id, parent2.lft, parent2.rgt]
assert_equal [parent1.id, 2, 3], [child.root_id, child.lft, child.rgt]
assert_equal [parent1.id, 5], [parent1.root_id, parent1.nested_set_span]
assert_equal [parent1.id, 1], [parent2.root_id, parent2.nested_set_span]
assert_equal [parent1.id, 1], [child.root_id, child.nested_set_span]
end
def test_move_a_child_to_root
@ -78,9 +80,9 @@ class IssueNestedSetTest < ActiveSupport::TestCase
parent1.reload
parent2.reload
assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
assert_equal [parent2.id, 1, 2], [parent2.root_id, parent2.lft, parent2.rgt]
assert_equal [child.id, 1, 2], [child.root_id, child.lft, child.rgt]
assert_equal [parent1.id, 1], [parent1.root_id, parent1.nested_set_span]
assert_equal [parent2.id, 1], [parent2.root_id, parent2.nested_set_span]
assert_equal [child.id, 1], [child.root_id, child.nested_set_span]
end
def test_move_a_child_to_another_issue
@ -94,9 +96,9 @@ class IssueNestedSetTest < ActiveSupport::TestCase
parent1.reload
parent2.reload
assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
assert_equal [parent2.id, 1, 4], [parent2.root_id, parent2.lft, parent2.rgt]
assert_equal [parent2.id, 2, 3], [child.root_id, child.lft, child.rgt]
assert_equal [parent1.id, 1], [parent1.root_id, parent1.nested_set_span]
assert_equal [parent2.id, 3], [parent2.root_id, parent2.nested_set_span]
assert_equal [parent2.id, 1], [child.root_id, child.nested_set_span]
end
def test_move_a_child_with_descendants_to_another_issue
@ -110,10 +112,10 @@ class IssueNestedSetTest < ActiveSupport::TestCase
child.reload
grandchild.reload
assert_equal [parent1.id, 1, 6], [parent1.root_id, parent1.lft, parent1.rgt]
assert_equal [parent2.id, 1, 2], [parent2.root_id, parent2.lft, parent2.rgt]
assert_equal [parent1.id, 2, 5], [child.root_id, child.lft, child.rgt]
assert_equal [parent1.id, 3, 4], [grandchild.root_id, grandchild.lft, grandchild.rgt]
assert_equal [parent1.id, 5], [parent1.root_id, parent1.nested_set_span]
assert_equal [parent2.id, 1], [parent2.root_id, parent2.nested_set_span]
assert_equal [parent1.id, 3], [child.root_id, child.nested_set_span]
assert_equal [parent1.id, 1], [grandchild.root_id, grandchild.nested_set_span]
child.reload.parent_issue_id = parent2.id
child.save!
@ -122,10 +124,10 @@ class IssueNestedSetTest < ActiveSupport::TestCase
parent1.reload
parent2.reload
assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
assert_equal [parent2.id, 1, 6], [parent2.root_id, parent2.lft, parent2.rgt]
assert_equal [parent2.id, 2, 5], [child.root_id, child.lft, child.rgt]
assert_equal [parent2.id, 3, 4], [grandchild.root_id, grandchild.lft, grandchild.rgt]
assert_equal [parent1.id, 1], [parent1.root_id, parent1.nested_set_span]
assert_equal [parent2.id, 5], [parent2.root_id, parent2.nested_set_span]
assert_equal [parent2.id, 3], [child.root_id, child.nested_set_span]
assert_equal [parent2.id, 1], [grandchild.root_id, grandchild.nested_set_span]
end
def test_move_a_child_with_descendants_to_another_project
@ -138,9 +140,9 @@ class IssueNestedSetTest < ActiveSupport::TestCase
grandchild.reload
parent1.reload
assert_equal [1, parent1.id, 1, 2], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt]
assert_equal [2, child.id, 1, 4], [child.project_id, child.root_id, child.lft, child.rgt]
assert_equal [2, child.id, 2, 3], [grandchild.project_id, grandchild.root_id, grandchild.lft, grandchild.rgt]
assert_equal [1, parent1.id, 1], [parent1.project_id, parent1.root_id, parent1.nested_set_span]
assert_equal [2, child.id, 3], [child.project_id, child.root_id, child.nested_set_span]
assert_equal [2, child.id, 1], [grandchild.project_id, grandchild.root_id, grandchild.nested_set_span]
end
def test_invalid_move_to_another_project
@ -150,7 +152,7 @@ class IssueNestedSetTest < ActiveSupport::TestCase
Project.find(2).tracker_ids = [1]
parent1.reload
assert_equal [1, parent1.id, 1, 6], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt]
assert_equal [1, parent1.id, 5], [parent1.project_id, parent1.root_id, parent1.nested_set_span]
# child can not be moved to Project 2 because its child is on a disabled tracker
assert_equal false, Issue.find(child.id).move_to_project(Project.find(2))
@ -159,9 +161,9 @@ class IssueNestedSetTest < ActiveSupport::TestCase
parent1.reload
# no change
assert_equal [1, parent1.id, 1, 6], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt]
assert_equal [1, parent1.id, 2, 5], [child.project_id, child.root_id, child.lft, child.rgt]
assert_equal [1, parent1.id, 3, 4], [grandchild.project_id, grandchild.root_id, grandchild.lft, grandchild.rgt]
assert_equal [1, parent1.id, 5], [parent1.project_id, parent1.root_id, parent1.nested_set_span]
assert_equal [1, parent1.id, 3], [child.project_id, child.root_id, child.nested_set_span]
assert_equal [1, parent1.id, 1], [grandchild.project_id, grandchild.root_id, grandchild.nested_set_span]
end
def test_moving_an_issue_to_a_descendant_should_not_validate
@ -212,8 +214,8 @@ class IssueNestedSetTest < ActiveSupport::TestCase
issue4.reload
assert !Issue.exists?(issue2.id)
assert !Issue.exists?(issue3.id)
assert_equal [issue1.id, 1, 4], [issue1.root_id, issue1.lft, issue1.rgt]
assert_equal [issue1.id, 2, 3], [issue4.root_id, issue4.lft, issue4.rgt]
assert_equal [issue1.id, 3], [issue1.root_id, issue1.nested_set_span]
assert_equal [issue1.id, 1], [issue4.root_id, issue4.nested_set_span]
end
def test_destroy_parent_issue_updated_during_children_destroy

@ -372,6 +372,7 @@ class IssueTest < ActiveSupport::TestCase
def test_move_to_another_project_should_clear_fixed_version_when_not_shared
issue = Issue.find(1)
issue.update_attribute(:fixed_version_id, 1)
issue.reload
assert issue.move_to_project(Project.find(2))
issue.reload
assert_equal 2, issue.project_id
@ -382,6 +383,7 @@ class IssueTest < ActiveSupport::TestCase
def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
issue = Issue.find(1)
issue.update_attribute(:fixed_version_id, 4)
issue.reload
assert issue.move_to_project(Project.find(5))
issue.reload
assert_equal 5, issue.project_id
@ -392,6 +394,7 @@ class IssueTest < ActiveSupport::TestCase
def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
issue = Issue.find(1)
issue.update_attribute(:fixed_version_id, 1)
issue.reload
assert issue.move_to_project(Project.find(5))
issue.reload
assert_equal 5, issue.project_id
@ -402,6 +405,7 @@ class IssueTest < ActiveSupport::TestCase
def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
issue = Issue.find(1)
issue.update_attribute(:fixed_version_id, 7)
issue.reload
assert issue.move_to_project(Project.find(2))
issue.reload
assert_equal 2, issue.project_id
@ -871,4 +875,22 @@ class IssueTest < ActiveSupport::TestCase
assert issue.save
assert_equal 0, ActionMailer::Base.deliveries.size
end
test 'changing the line endings in a description will not be recorded as a Journal' do
User.current = User.find(1)
issue = Issue.find(1)
issue.update_attribute(:description, "Description with newlines\n\nembedded")
issue.reload
assert issue.description.include?("\n")
assert_no_difference("Journal.count") do
issue.safe_attributes= {
'description' => "Description with newlines\r\n\r\nembedded"
}
assert issue.save
end
assert_equal "Description with newlines\n\nembedded", issue.reload.description
end
end

@ -13,103 +13,97 @@
require File.expand_path('../../test_helper', __FILE__)
class JournalObserverTest < ActiveSupport::TestCase
fixtures :issues, :issue_statuses, :journals
def setup
@user = User.generate!(:mail_notification => 'all')
@project = Project.generate!
User.add_to_project(@user, @project, Role.generate!(:permissions => [:view_issues, :edit_issues]))
@issue = Issue.generate_for_project!(@project)
ActionMailer::Base.deliveries.clear
@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
# context: issue_updated notified_events
def test_create_should_send_email_notification_with_issue_updated
context "#after_create for 'issue_updated'" do
should "should send a notification when configured as a notification" do
Setting.notified_events = ['issue_updated']
issue = Issue.find(:first)
user = User.find(:first)
issue.init_journal(user)
assert issue.send(:create_journal)
assert_equal 1, ActionMailer::Base.deliveries.size
assert_difference('ActionMailer::Base.deliveries.size') do
@issue.init_journal(@user)
@issue.subject = "A change to the issue"
assert @issue.save
end
end
def test_create_should_not_send_email_notification_without_issue_updated
should "not send a notification with not configured" do
Setting.notified_events = []
issue = Issue.find(:first)
user = User.find(:first)
issue.init_journal(user)
assert_no_difference('ActionMailer::Base.deliveries.size') do
@issue.init_journal(@user)
@issue.subject = "A change to the issue"
assert @issue.save
end
end
assert issue.save
assert_equal 0, ActionMailer::Base.deliveries.size
end
# context: issue_note_added notified_events
def test_create_should_send_email_notification_with_issue_note_added
context "#after_create for 'issue_note_added'" do
should "should send a notification when configured as a notification" do
Setting.notified_events = ['issue_note_added']
issue = Issue.find(:first)
user = User.find(:first)
issue.init_journal(user, 'This update has a note')
assert_difference('ActionMailer::Base.deliveries.size') do
@issue.init_journal(@user, 'This update has a note')
assert @issue.save
end
assert issue.save
assert_equal 1, ActionMailer::Base.deliveries.size
end
def test_create_should_not_send_email_notification_without_issue_note_added
should "not send a notification with not configured" do
Setting.notified_events = []
issue = Issue.find(:first)
user = User.find(:first)
issue.init_journal(user, 'This update has a note')
assert_no_difference('ActionMailer::Base.deliveries.size') do
@issue.init_journal(@user, 'This update has a note')
assert @issue.save
end
assert issue.save
assert_equal 0, ActionMailer::Base.deliveries.size
end
end
# context: issue_status_updated notified_events
def test_create_should_send_email_notification_with_issue_status_updated
context "#after_create for 'issue_status_updated'" do
should "should send a notification when configured as a notification" do
Setting.notified_events = ['issue_status_updated']
issue = Issue.find(:first)
user = User.find(:first)
issue.init_journal(user)
issue.status = IssueStatus.last
assert_difference('ActionMailer::Base.deliveries.size') do
@issue.init_journal(@user)
@issue.status = IssueStatus.generate!
assert @issue.save
assert issue.save
assert_equal 1, ActionMailer::Base.deliveries.size
end
def test_create_should_not_send_email_notification_without_issue_status_updated
end
should "not send a notification with not configured" do
Setting.notified_events = []
issue = Issue.find(:first)
user = User.find(:first)
issue.init_journal(user)
issue.status = IssueStatus.last
assert_no_difference('ActionMailer::Base.deliveries.size') do
@issue.init_journal(@user)
@issue.status = IssueStatus.generate!
assert @issue.save
assert issue.save
assert_equal 0, ActionMailer::Base.deliveries.size
end
end
end
# context: issue_priority_updated notified_events
def test_create_should_send_email_notification_with_issue_priority_updated
context "#after_create for 'issue_priority_updated'" do
should "should send a notification when configured as a notification" do
Setting.notified_events = ['issue_priority_updated']
issue = Issue.find(:first)
user = User.find(:first)
issue.init_journal(user)
issue.priority = IssuePriority.last
assert_difference('ActionMailer::Base.deliveries.size') do
@issue.init_journal(@user)
@issue.priority = IssuePriority.generate!
assert @issue.save
end
assert issue.save
assert_equal 1, ActionMailer::Base.deliveries.size
end
def test_create_should_not_send_email_notification_without_issue_priority_updated
should "not send a notification with not configured" do
Setting.notified_events = []
issue = Issue.find(:first)
user = User.find(:first)
issue.init_journal(user)
issue.priority = IssuePriority.last
assert issue.save
assert_equal 0, ActionMailer::Base.deliveries.size
assert_no_difference('ActionMailer::Base.deliveries.size') do
@issue.init_journal(@user)
@issue.priority = IssuePriority.generate!
assert @issue.save
end
end
end
end

@ -36,12 +36,13 @@ class JournalTest < ActiveSupport::TestCase
ActionMailer::Base.deliveries.clear
issue = Issue.find(:first)
if issue.journals.empty?
issue.init_journal(User.current, "This journal represents the creational journal version 1")
issue.init_journal(User.current, "This journal represents the creationa of journal version 1")
issue.save
end
user = User.find(:first)
assert_equal 0, ActionMailer::Base.deliveries.size
issue.reload
issue.update_attribute(:subject, "New subject to trigger automatic journal entry")
assert_equal 1, ActionMailer::Base.deliveries.size
end
@ -58,4 +59,39 @@ class JournalTest < ActiveSupport::TestCase
end
assert_equal 0, ActionMailer::Base.deliveries.size
end
test "creating the initial journal should track the changes from creation" do
@project = Project.generate!
issue = Issue.new do |i|
i.project = @project
i.subject = "Test initial journal"
i.tracker = @project.trackers.first
i.author = User.generate!
i.description = "Some content"
end
assert_difference("Journal.count") do
assert issue.save
end
journal = issue.reload.journals.first
assert_equal ["","Test initial journal"], journal.changes["subject"]
assert_equal [0, @project.id], journal.changes["project_id"]
assert_equal [nil, "Some content"], journal.changes["description"]
end
test "creating a journal should update the updated_on value of the parent record (touch)" do
@user = User.generate!
@project = Project.generate!
@issue = Issue.generate_for_project!(@project).reload
start = @issue.updated_on
sleep(1) # TODO: massive hack to make sure the timestamps are different. switch to timecop later
assert_difference("Journal.count") do
@issue.init_journal(@user, "A note")
@issue.save
end
assert_not_equal start, @issue.reload.updated_on
end
end

@ -60,7 +60,8 @@ class MailHandlerTest < ActiveSupport::TestCase
assert_equal Version.find_by_name('alpha'), issue.fixed_version
assert_equal 2.5, issue.estimated_hours
assert_equal 30, issue.done_ratio
assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
assert_equal issue.id, issue.root_id
assert issue.leaf?
# keywords should be removed from the email body
assert !issue.description.match(/^Project:/i)
assert !issue.description.match(/^Status:/i)
@ -208,7 +209,8 @@ class MailHandlerTest < ActiveSupport::TestCase
assert issue.is_a?(Issue)
assert issue.author.anonymous?
assert !issue.project.is_public?
assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
assert_equal issue.id, issue.root_id
assert issue.leaf?
end
end
end
@ -299,6 +301,15 @@ class MailHandlerTest < ActiveSupport::TestCase
assert_equal 'Feature request', journal.issue.tracker.name
end
test "reply to issue update (Journal) by message_id" do
journal = submit_email('ticket_reply_by_message_id.eml')
assert journal.is_a?(IssueJournal), "Email was a #{journal.class}"
assert_equal User.find_by_login('jsmith'), journal.user
assert_equal Issue.find(2), journal.journaled
assert_match /This is reply/, journal.notes
assert_equal 'Feature request', journal.issue.tracker.name
end
def test_add_issue_note_with_attribute_changes
# This email contains: 'Status: Resolved'
journal = submit_email('ticket_reply_with_status.eml')

@ -135,8 +135,8 @@ module Redmine
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[: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|
@ -173,7 +173,8 @@ module Redmine
end
end
options[:type] ||= self.name.underscore.dasherize # Make sure the name of the journalized model and not the name of the journal is used for events
{ :description => :notes, :author => :user }.reverse_merge options
options[:author] ||= :user
{ :description => :notes }.reverse_merge options
end
end

@ -45,7 +45,7 @@ module Redmine::Acts::Journalized
base.class_eval do
include InstanceMethods
after_update :merge_journal_changes
after_save :merge_journal_changes
end
end

Loading…
Cancel
Save