Merge branch 'stable' into unstable

This commit is contained in:
Eric Davis 2011-07-29 13:54:27 -07:00
commit 74cfd7b5ca
54 changed files with 438 additions and 262 deletions

1
.gitignore vendored
View File

@ -26,4 +26,5 @@
doc/app doc/app
/.bundle /.bundle
/Gemfile.lock /Gemfile.lock
/Gemfile.local
/.rvmrc* /.rvmrc*

27
Gemfile
View File

@ -11,6 +11,9 @@ group :test do
gem 'shoulda', '~> 2.10.3' gem 'shoulda', '~> 2.10.3'
gem 'edavis10-object_daddy', :require => 'object_daddy' gem 'edavis10-object_daddy', :require => 'object_daddy'
gem 'mocha' gem 'mocha'
platforms :mri_18 do gem 'ruby-debug' end
platforms :mri_19 do gem 'ruby-debug19', :require => 'ruby-debug' end
end end
group :openid do group :openid do
@ -36,15 +39,22 @@ platforms :mri do
group :mysql2 do group :mysql2 do
gem "mysql2", "~> 0.2.7" gem "mysql2", "~> 0.2.7"
end end
group :postgres do group :postgres do
gem "pg", "~> 0.9.0" gem "pg", "~> 0.9.0"
# gem "postgres-pr" # gem "postgres-pr"
end end
end
platforms :mri_18 do
group :sqlite do group :sqlite do
gem "sqlite3-ruby", "< 1.3", :require => "sqlite3" gem "sqlite3-ruby", "< 1.3", :require => "sqlite3"
# please tell me, if you are fond of a pure ruby sqlite3 binding end
end
platforms :mri_19 do
group :sqlite do
gem "sqlite3"
end end
end end
@ -54,16 +64,23 @@ platforms :jruby do
group :mysql do group :mysql do
gem "activerecord-jdbcmysql-adapter" gem "activerecord-jdbcmysql-adapter"
end end
group :postgres do group :postgres do
gem "activerecord-jdbcpostgresql-adapter" gem "activerecord-jdbcpostgresql-adapter"
end end
group :sqlite do group :sqlite do
gem "activerecord-jdbcsqlite3-adapter" gem "activerecord-jdbcsqlite3-adapter"
end end
end end
# Load a "local" Gemfile
gemfile_local = File.join(File.dirname(__FILE__), "Gemfile.local")
if File.readable?(gemfile_local)
puts "Loading #{gemfile_local} ..." if $DEBUG
instance_eval(File.read(gemfile_local))
end
# Load plugins' Gemfiles # Load plugins' Gemfiles
Dir.glob File.expand_path("../vendor/plugins/*/Gemfile", __FILE__) do |file| Dir.glob File.expand_path("../vendor/plugins/*/Gemfile", __FILE__) do |file|
puts "Loading #{file} ..." if $DEBUG # `ruby -d` or `bundle -v` puts "Loading #{file} ..." if $DEBUG # `ruby -d` or `bundle -v`

View File

@ -24,6 +24,12 @@ class ApplicationController < ActionController::Base
layout 'base' layout 'base'
exempt_from_layout 'builder', 'rsb' exempt_from_layout 'builder', 'rsb'
protect_from_forgery
def handle_unverified_request
super
cookies.delete(:autologin)
end
# Remove broken cookie after upgrade from 0.8.x (#4292) # Remove broken cookie after upgrade from 0.8.x (#4292)
# See https://rails.lighthouseapp.com/projects/8994/tickets/3360 # See https://rails.lighthouseapp.com/projects/8994/tickets/3360
# TODO: remove it when Rails is fixed # TODO: remove it when Rails is fixed
@ -38,7 +44,6 @@ class ApplicationController < ActionController::Base
before_filter :user_setup, :check_if_login_required, :set_localization before_filter :user_setup, :check_if_login_required, :set_localization
filter_parameter_logging :password filter_parameter_logging :password
protect_from_forgery
rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token

View File

@ -286,7 +286,7 @@ private
render_error l(:error_no_tracker_in_project) render_error l(:error_no_tracker_in_project)
return false return false
end end
@issue.start_date ||= Date.today @issue.start_date ||= User.current.today
if params[:issue].is_a?(Hash) if params[:issue].is_a?(Hash)
@issue.safe_attributes = params[:issue] @issue.safe_attributes = params[:issue]
if User.current.allowed_to?(:add_issue_watchers, @project) && @issue.new_record? if User.current.allowed_to?(:add_issue_watchers, @project) && @issue.new_record?

View File

@ -16,10 +16,12 @@ class ProjectsController < ApplicationController
menu_item :roadmap, :only => :roadmap menu_item :roadmap, :only => :roadmap
menu_item :settings, :only => :settings menu_item :settings, :only => :settings
before_filter :find_project, :except => [ :index, :list, :new, :create, :copy ] before_filter :find_project, :except => [ :index, :new, :create, :copy ]
before_filter :authorize, :except => [ :index, :list, :new, :create, :copy, :archive, :unarchive, :destroy] before_filter :authorize, :only => [ :show, :settings, :edit, :update, :modules ]
before_filter :authorize_global, :only => [:new, :create] before_filter :authorize_global, :only => [:new, :create]
before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ] before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
before_filter :jump_to_project_menu_item, :only => :show
before_filter :load_project_settings, :only => :settings
accept_key_auth :index, :show, :create, :update, :destroy accept_key_auth :index, :show, :create, :update, :destroy
after_filter :only => [:create, :edit, :update, :archive, :unarchive, :destroy] do |controller| after_filter :only => [:create, :edit, :update, :archive, :unarchive, :destroy] do |controller|
@ -68,12 +70,7 @@ class ProjectsController < ApplicationController
if validate_parent_id && @project.save if validate_parent_id && @project.save
@project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id') @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
# Add current user as a project member if he is not admin add_current_user_to_project_if_not_admin(@project)
unless User.current.admin?
r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
m = Member.new(:user => User.current, :roles => [r])
@project.members << m
end
respond_to do |format| respond_to do |format|
format.html { format.html {
flash[:notice] = l(:notice_successful_create) flash[:notice] = l(:notice_successful_create)
@ -128,11 +125,6 @@ class ProjectsController < ApplicationController
# Show @project # Show @project
def show def show
if params[:jump]
# try to redirect to the requested menu item
redirect_to_project_menu_item(@project, params[:jump]) && return
end
@users_by_role = @project.users_by_role @users_by_role = @project.users_by_role
@subprojects = @project.children.visible.all @subprojects = @project.children.visible.all
@news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC") @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
@ -151,8 +143,6 @@ class ProjectsController < ApplicationController
@total_hours = TimeEntry.visible.sum(:hours, :include => :project, :conditions => cond).to_f @total_hours = TimeEntry.visible.sum(:hours, :include => :project, :conditions => cond).to_f
end end
@key = User.current.rss_key
respond_to do |format| respond_to do |format|
format.html format.html
format.api format.api
@ -160,12 +150,6 @@ class ProjectsController < ApplicationController
end end
def settings def settings
@issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
@issue_category ||= IssueCategory.new
@member ||= @project.members.new
@trackers = Tracker.all
@repository ||= @project.repository
@wiki ||= @project.wiki
end end
def edit def edit
@ -187,7 +171,7 @@ class ProjectsController < ApplicationController
else else
respond_to do |format| respond_to do |format|
format.html { format.html {
settings load_project_settings
render :action => 'settings' render :action => 'settings'
} }
format.api { render_validation_errors(@project) } format.api { render_validation_errors(@project) }
@ -230,8 +214,7 @@ class ProjectsController < ApplicationController
end end
end end
end end
# hide project in layout hide_project_in_layout
@project = nil
end end
private private
@ -257,4 +240,33 @@ private
end end
true true
end end
def jump_to_project_menu_item
if params[:jump]
# try to redirect to the requested menu item
redirect_to_project_menu_item(@project, params[:jump]) && return
end
end
def load_project_settings
@issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
@issue_category ||= IssueCategory.new
@member ||= @project.members.new
@trackers = Tracker.all
@repository ||= @project.repository
@wiki ||= @project.wiki
end
def hide_project_in_layout
@project = nil
end
def add_current_user_to_project_if_not_admin(project)
unless User.current.admin?
r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
m = Member.new(:user => User.current, :roles => [r])
project.members << m
end
end
end end

View File

@ -143,7 +143,12 @@ class RepositoriesController < ApplicationController
return true if Redmine::MimeType.is_type?('text', path) return true if Redmine::MimeType.is_type?('text', path)
# Ruby 1.8.6 has a bug of integer divisions. # Ruby 1.8.6 has a bug of integer divisions.
# http://apidock.com/ruby/v1_8_6_287/String/is_binary_data%3F # http://apidock.com/ruby/v1_8_6_287/String/is_binary_data%3F
return false if ent.is_binary_data? if ent.respond_to?("is_binary_data?") && ent.is_binary_data? # Ruby 1.8.x and <1.9.2
return false
elsif ent.respond_to?(:force_encoding) && (ent.dup.force_encoding("UTF-8") != ent.dup.force_encoding("BINARY") ) # Ruby 1.9.2
# TODO: need to handle edge cases of non-binary content that isn't UTF-8
return false
end
true true
end end
private :is_entry_text_data? private :is_entry_text_data?

View File

@ -81,7 +81,7 @@ module ApplicationHelper
subject = truncate(subject, :length => options[:truncate]) subject = truncate(subject, :length => options[:truncate])
end end
end end
s = link_to "#{issue.tracker} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
:class => issue.css_classes, :class => issue.css_classes,
:title => title :title => title
s << ": #{h subject}" if subject s << ": #{h subject}" if subject

View File

@ -129,82 +129,6 @@ module IssuesHelper
out out
end end
def show_detail(detail, no_html=false)
case detail.property
when 'attr'
field = detail.prop_key.to_s.gsub(/\_id$/, "")
label = l(("field_" + field).to_sym)
case
when ['due_date', 'start_date'].include?(detail.prop_key)
value = format_date(detail.value.to_date) if detail.value
old_value = format_date(detail.old_value.to_date) if detail.old_value
when ['project_id', 'status_id', 'tracker_id', 'assigned_to_id', 'priority_id', 'category_id', 'fixed_version_id'].include?(detail.prop_key)
value = find_name_by_reflection(field, detail.value)
old_value = find_name_by_reflection(field, detail.old_value)
when detail.prop_key == 'estimated_hours'
value = "%0.02f" % detail.value.to_f unless detail.value.blank?
old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
when detail.prop_key == 'parent_id'
label = l(:field_parent_issue)
value = "##{detail.value}" unless detail.value.blank?
old_value = "##{detail.old_value}" unless detail.old_value.blank?
end
when 'cf'
custom_field = CustomField.find_by_id(detail.prop_key)
if custom_field
label = custom_field.name
value = format_value(detail.value, custom_field.field_format) if detail.value
old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
end
when 'attachment'
label = l(:label_attachment)
end
call_hook(:helper_issues_show_detail_after_setting, {:detail => detail, :label => label, :value => value, :old_value => old_value })
label ||= detail.prop_key
value ||= detail.value
old_value ||= detail.old_value
unless no_html
label = content_tag('strong', label)
old_value = content_tag("i", h(old_value)) if detail.old_value
old_value = content_tag("strike", old_value) if detail.old_value and (!detail.value or detail.value.empty?)
if detail.property == 'attachment' && !value.blank? && a = Attachment.find_by_id(detail.prop_key)
# Link to the attachment if it has not been removed
value = link_to_attachment(a)
else
value = content_tag("i", h(value)) if value
end
end
if detail.property == 'attr' && detail.prop_key == 'description'
s = l(:text_journal_changed_no_detail, :label => label)
unless no_html
diff_link = link_to 'diff',
{:controller => 'journals', :action => 'diff', :id => detail.journal_id, :detail_id => detail.id},
:title => l(:label_view_diff)
s << " (#{ diff_link })"
end
s
elsif !detail.value.blank?
case detail.property
when 'attr', 'cf'
if !detail.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
when 'attachment'
l(:text_journal_added, :label => label, :value => value)
end
else
l(:text_journal_deleted, :label => label, :old => old_value)
end
end
# Find the name of an associated record stored in the field attribute # Find the name of an associated record stored in the field attribute
def find_name_by_reflection(field, id) def find_name_by_reflection(field, id)
association = Issue.reflect_on_association(field.to_sym) association = Issue.reflect_on_association(field.to_sym)

View File

@ -48,7 +48,7 @@ module JournalsHelper
if d = journal.render_detail(detail) if d = journal.render_detail(detail)
content_tag("li", d) content_tag("li", d)
end end
end.compact end.compact.join(' ')
end end
end end

View File

@ -57,4 +57,14 @@ module SearchHelper
end end
('<ul>' + links.map {|link| content_tag('li', link)}.join(' ') + '</ul>') unless links.empty? ('<ul>' + links.map {|link| content_tag('li', link)}.join(' ') + '</ul>') unless links.empty?
end end
def link_to_previous_search_page(pagination_previous_date)
link_to_content_update('&#171; ' + l(:label_previous),
params.merge(:previous => 1, :offset => pagination_previous_date.strftime("%Y%m%d%H%M%S")))
end
def link_to_next_search_page(pagination_next_date)
link_to_content_update(l(:label_next) + ' &#187;',
params.merge(:previous => nil, :offset => pagination_next_date.strftime("%Y%m%d%H%M%S")))
end
end end

View File

@ -17,10 +17,22 @@ class Change < ActiveRecord::Base
validates_presence_of :changeset_id, :action, :path validates_presence_of :changeset_id, :action, :path
before_save :init_path before_save :init_path
delegate :repository_encoding, :to => :changeset, :allow_nil => true, :prefix => true
def relative_path def relative_path
changeset.repository.relative_path(path) changeset.repository.relative_path(path)
end end
def path
# TODO: shouldn't access Changeset#to_utf8 directly
self.path = Changeset.to_utf8(read_attribute(:path), changeset_repository_encoding)
end
def from_path
# TODO: shouldn't access Changeset#to_utf8 directly
self.path = Changeset.to_utf8(read_attribute(:from_path), changeset_repository_encoding)
end
def init_path def init_path
self.path ||= "" self.path ||= ""
end end

View File

@ -74,6 +74,23 @@ class Changeset < ActiveRecord::Base
user || committer.to_s.split('<').first user || committer.to_s.split('<').first
end end
# Delegate to a Repository's log encoding
def repository_encoding
if repository.present?
repository.repo_log_encoding
else
nil
end
end
# Committer of the Changeset
#
# Attribute reader for committer that encodes the committer string to
# the repository log encoding (e.g. UTF-8)
def committer
self.class.to_utf8(read_attribute(:committer), repository.repo_log_encoding)
end
def before_create def before_create
self.committer = self.class.to_utf8(self.committer, repository.repo_log_encoding) self.committer = self.class.to_utf8(self.committer, repository.repo_log_encoding)
self.comments = self.class.normalize_comments(self.comments, repository.repo_log_encoding) self.comments = self.class.normalize_comments(self.comments, repository.repo_log_encoding)
@ -239,6 +256,7 @@ class Changeset < ActiveRecord::Base
private private
# TODO: refactor to a standard helper method
def self.to_utf8(str, encoding) def self.to_utf8(str, encoding)
return str if str.nil? return str if str.nil?
str.force_encoding("ASCII-8BIT") if str.respond_to?(:force_encoding) str.force_encoding("ASCII-8BIT") if str.respond_to?(:force_encoding)
@ -273,12 +291,6 @@ class Changeset < ActiveRecord::Base
end end
str = txtar str = txtar
end end
# removes invalid UTF8 sequences str
begin
Iconv.conv('UTF-8//IGNORE', 'UTF-8', str + ' ')[0..-3]
rescue Iconv::InvalidEncoding
# "UTF-8//IGNORE" is not supported on some OS
str
end
end end
end end

View File

@ -23,9 +23,17 @@ class Journal < ActiveRecord::Base
# Make sure each journaled model instance only has unique version ids # Make sure each journaled model instance only has unique version ids
validates_uniqueness_of :version, :scope => [:journaled_id, :type] validates_uniqueness_of :version, :scope => [:journaled_id, :type]
belongs_to :journaled, :touch => true
# Define a default class_name to prevent `uninitialized constant Journal::Journaled`
# subclasses will be given an actual class name when they are created by aaj
#
# e.g. IssueJournal will get :class_name => 'Issue'
belongs_to :journaled, :class_name => 'Journal'
belongs_to :user belongs_to :user
# "touch" the journaled object on creation
after_create :touch_journaled_after_creation
# ActiveRecord::Base#changes is an existing method, so before serializing the +changes+ column, # 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 # 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 # dirty attributes, but will not affect the partial updates functionality as that's based on
@ -33,6 +41,10 @@ class Journal < ActiveRecord::Base
# undef_method :changes # undef_method :changes
serialize :changes, Hash serialize :changes, Hash
def touch_journaled_after_creation
journaled.touch
end
# In conjunction with the included Comparable module, allows comparison of journal records # In conjunction with the included Comparable module, allows comparison of journal records
# based on their corresponding version numbers, creation timestamps and IDs. # based on their corresponding version numbers, creation timestamps and IDs.
def <=>(other) def <=>(other)

View File

@ -185,7 +185,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.last_journal.event_url) :message_url => url_for({ :controller => 'messages', :action => 'show', :board_id => message.board, :id => message.root, :r => message, :anchor => "message-#{message.id}" })
render_multipart('message_posted', body) render_multipart('message_posted', body)
end end

View File

@ -12,10 +12,7 @@
#++ #++
class MessageObserver < ActiveRecord::Observer class MessageObserver < ActiveRecord::Observer
def after_save(message) def after_create(message)
if message.last_journal.version == 1 Mailer.deliver_message_posted(message) if Setting.notified_events.include?('message_posted')
# Only deliver mails for the first journal
Mailer.deliver_message_posted(message) if Setting.notified_events.include?('message_posted')
end
end end
end end

View File

@ -126,8 +126,9 @@ class Query < ActiveRecord::Base
QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'), QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true), QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true), QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"), # Put empty start_dates and due_dates in the far future rather than in the far past
QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"), QueryColumn.new(:start_date, :sortable => ["CASE WHEN #{Issue.table_name}.start_date IS NULL THEN 1 ELSE 0 END", "#{Issue.table_name}.start_date"]),
QueryColumn.new(:due_date, :sortable => ["CASE WHEN #{Issue.table_name}.due_date IS NULL THEN 1 ELSE 0 END", "#{Issue.table_name}.due_date"]),
QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"), QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true), QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'), QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
@ -587,9 +588,17 @@ class Query < ActiveRecord::Base
sql = "#{db_table}.#{db_field} IS NOT NULL" sql = "#{db_table}.#{db_field} IS NOT NULL"
sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
when ">=" when ">="
sql = "#{db_table}.#{db_field} >= #{value.first.to_i}" if is_custom_filter
sql = "#{db_table}.#{db_field} != '' AND CAST(#{db_table}.#{db_field} AS decimal(60,4)) >= #{value.first.to_f}"
else
sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
end
when "<=" when "<="
sql = "#{db_table}.#{db_field} <= #{value.first.to_i}" if is_custom_filter
sql = "#{db_table}.#{db_field} != '' AND CAST(#{db_table}.#{db_field} AS decimal(60,4)) <= #{value.first.to_f}"
else
sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
end
when "o" when "o"
sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id" sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
when "c" when "c"
@ -629,6 +638,8 @@ class Query < ActiveRecord::Base
custom_fields.select(&:is_filter?).each do |field| custom_fields.select(&:is_filter?).each do |field|
case field.field_format case field.field_format
when "int", "float"
options = { :type => :integer, :order => 20 }
when "text" when "text"
options = { :type => :text, :order => 20 } options = { :type => :text, :order => 20 }
when "list" when "list"

View File

@ -59,12 +59,7 @@ class WikiContent < ActiveRecord::Base
end end
def version def version
unless last_journal new_record? ? 0 : last_journal.version
# 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 end
private private
@ -106,9 +101,9 @@ class WikiContent < ActiveRecord::Base
end end
def text def text
@text ||= case changes[:compression] @text ||= case changes["compression"]
when 'gzip' when "gzip"
Zlib::Inflate.inflate(data) Zlib::Inflate.inflate(changes["data"])
else else
# uncompressed data # uncompressed data
changes["data"] changes["data"]

View File

@ -1,6 +1,6 @@
<%= render :partial => 'action_menu' %> <%= render :partial => 'action_menu' %>
<h2><%= @issue.tracker.name %> #<%= @issue.id %><%= call_hook(:view_issues_show_identifier, :issue => @issue) %></h2> <h2><%= h(@issue.tracker.name) %> #<%= h(@issue.id) %><%= call_hook(:view_issues_show_identifier, :issue => @issue) %></h2>
<div class="<%= @issue.css_classes %> details"> <div class="<%= @issue.css_classes %> details">
<%= avatar(@issue.author, :size => "50") %> <%= avatar(@issue.author, :size => "50") %>
@ -17,11 +17,11 @@
<table class="attributes"> <table class="attributes">
<tr> <tr>
<th class="status"><%=l(:field_status)%>:</th><td class="status"><%= @issue.status.name %></td> <th class="status"><%=l(:field_status)%>:</th><td class="status"><%= h(@issue.status.name) %></td>
<th class="start-date"><%=l(:field_start_date)%>:</th><td class="start-date"><%= format_date(@issue.start_date) %></td> <th class="start-date"><%=l(:field_start_date)%>:</th><td class="start-date"><%= format_date(@issue.start_date) %></td>
</tr> </tr>
<tr> <tr>
<th class="priority"><%=l(:field_priority)%>:</th><td class="priority"><%= @issue.priority.name %></td> <th class="priority"><%=l(:field_priority)%>:</th><td class="priority"><%= h(@issue.priority.name) %></td>
<th class="due-date"><%=l(:field_due_date)%>:</th><td class="due-date"><%= format_date(@issue.due_date) %></td> <th class="due-date"><%=l(:field_due_date)%>:</th><td class="due-date"><%= format_date(@issue.due_date) %></td>
</tr> </tr>
<tr> <tr>
@ -29,7 +29,7 @@
<th class="progress"><%=l(:field_done_ratio)%>:</th><td class="progress"><%= progress_bar @issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%" %></td> <th class="progress"><%=l(:field_done_ratio)%>:</th><td class="progress"><%= progress_bar @issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%" %></td>
</tr> </tr>
<tr> <tr>
<th class="category"><%=l(:field_category)%>:</th><td class="category"><%=h @issue.category ? @issue.category.name : "-" %></td> <th class="category"><%=l(:field_category)%>:</th><td class="category"><%=h(@issue.category ? @issue.category.name : "-") %></td>
<% if User.current.allowed_to?(:view_time_entries, @project) %> <% if User.current.allowed_to?(:view_time_entries, @project) %>
<th class="spent-time"><%=l(:label_spent_time)%>:</th> <th class="spent-time"><%=l(:label_spent_time)%>:</th>
<td class="spent-time"><%= @issue.spent_hours > 0 ? (link_to l_hours(@issue.spent_hours), {:controller => 'timelog', :action => 'index', :project_id => @project, :issue_id => @issue}) : "-" %></td> <td class="spent-time"><%= @issue.spent_hours > 0 ? (link_to l_hours(@issue.spent_hours), {:controller => 'timelog', :action => 'index', :project_id => @project, :issue_id => @issue}) : "-" %></td>

View File

@ -22,6 +22,6 @@
<td class="revision"><%= link_to_revision(changeset, @project) if changeset %></td> <td class="revision"><%= link_to_revision(changeset, @project) if changeset %></td>
<td class="age"><%= distance_of_time_in_words(entry.lastrev.time, Time.now) if entry.lastrev && entry.lastrev.time %></td> <td class="age"><%= distance_of_time_in_words(entry.lastrev.time, Time.now) if entry.lastrev && entry.lastrev.time %></td>
<td class="author"><%= changeset.nil? ? h(replace_invalid_utf8(entry.lastrev.author.to_s.split('<').first)) : changeset.author if entry.lastrev %></td> <td class="author"><%= changeset.nil? ? h(replace_invalid_utf8(entry.lastrev.author.to_s.split('<').first)) : changeset.author if entry.lastrev %></td>
<td class="comments"><%=h truncate(changeset.comments, :length => 50) unless changeset.nil? %></td> <td class="comments"><%=h truncate(Changeset.to_utf8(changeset.comments, changeset.repository.repo_log_encoding), :length => 50) unless changeset.nil? %></td>
</tr> </tr>
<% end %> <% end %>

View File

@ -18,7 +18,7 @@
<td class="checkbox"><%= radio_button_tag('rev_to', changeset.identifier, (line_num==2), :id => "cbto-#{line_num}", :onclick => "if ($('cb-#{line_num}').checked==true) {$('cb-#{line_num-1}').checked=true;}") if show_diff && (line_num > 1) %></td> <td class="checkbox"><%= radio_button_tag('rev_to', changeset.identifier, (line_num==2), :id => "cbto-#{line_num}", :onclick => "if ($('cb-#{line_num}').checked==true) {$('cb-#{line_num-1}').checked=true;}") if show_diff && (line_num > 1) %></td>
<td class="committed_on"><%= format_time(changeset.committed_on) %></td> <td class="committed_on"><%= format_time(changeset.committed_on) %></td>
<td class="author"><%=h changeset.author %></td> <td class="author"><%=h changeset.author %></td>
<td class="comments"><%= textilizable(truncate_at_line_break(changeset.comments)) %></td> <td class="comments"><%= textilizable(truncate_at_line_break(Changeset.to_utf8(changeset.comments, changeset.repository.repo_log_encoding))) %></td>
</tr> </tr>
<% line_num += 1 %> <% line_num += 1 %>
<% end %> <% end %>

View File

@ -0,0 +1,10 @@
<div class="search-pagination">
<p>
<% if pagination_previous_date %>
<%= link_to_previous_search_page(pagination_previous_date) %>
<% end %>
<% if pagination_next_date %>
<%= link_to_next_search_page(pagination_next_date) %>
<% end %>
</p>
</div>

View File

@ -24,6 +24,9 @@
</div> </div>
<h3><%= l(:label_result_plural) %> (<%= @results_by_type.values.sum %>)</h3> <h3><%= l(:label_result_plural) %> (<%= @results_by_type.values.sum %>)</h3>
<%= render :partial => 'pagination', :locals => {:pagination_previous_date => @pagination_previous_date, :pagination_next_date => @pagination_next_date } %>
<dl id="search-results"> <dl id="search-results">
<% @results.each do |e| %> <% @results.each do |e| %>
<dt class="<%= e.event_type %>"> <dt class="<%= e.event_type %>">
@ -36,15 +39,6 @@
</dl> </dl>
<% end %> <% end %>
<p><center> <%= render :partial => 'pagination', :locals => {:pagination_previous_date => @pagination_previous_date, :pagination_next_date => @pagination_next_date } %>
<% if @pagination_previous_date %>
<%= link_to_content_update('&#171; ' + l(:label_previous),
params.merge(:previous => 1, :offset => @pagination_previous_date.strftime("%Y%m%d%H%M%S"))) %>&nbsp;
<% end %>
<% if @pagination_next_date %>
<%= link_to_content_update(l(:label_next) + ' &#187;',
params.merge(:previous => nil, :offset => @pagination_next_date.strftime("%Y%m%d%H%M%S"))) %>
<% end %>
</center></p>
<% html_title(l(:label_search)) -%> <% html_title(l(:label_search)) -%>

View File

@ -3,6 +3,7 @@
<head> <head>
<title><%=h @page.pretty_title %></title> <title><%=h @page.pretty_title %></title>
<meta http-equiv="content-type" content="text/html; charset=utf-8" /> <meta http-equiv="content-type" content="text/html; charset=utf-8" />
<base href="<%= "#{h Setting.protocol}://#{h Setting.host_name}" %>" />
<style> <style>
body { font:80% Verdana,Tahoma,Arial,sans-serif; } body { font:80% Verdana,Tahoma,Arial,sans-serif; }
h1, h2, h3, h4 { font-family: "Trebuchet MS",Georgia,"Times New Roman",serif; } h1, h2, h3, h4 { font-family: "Trebuchet MS",Georgia,"Times New Roman",serif; }
@ -16,6 +17,6 @@ h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display
</style> </style>
</head> </head>
<body> <body>
<%= textilizable @content, :text, :wiki_links => :local %> <%= textilizable @content, :text, :wiki_links => :local, :only_path => false %>
</body> </body>
</html> </html>

View File

@ -945,21 +945,21 @@ bg:
enumeration_activities: Дейности (time tracking) enumeration_activities: Дейности (time tracking)
enumeration_system_activity: Системна активност enumeration_system_activity: Системна активност
text_powered_by: Powered by %{link} text_powered_by: Този сайт е задвижван от %{link}
label_cvs_module: Module label_cvs_module: Модул
label_filesystem_path: Root directory label_filesystem_path: Коренна директория
label_darcs_path: Root directory label_darcs_path: Коренна директория
label_bazaar_path: Root directory label_bazaar_path: Коренна директория
label_cvs_path: CVSROOT label_cvs_path: CVSROOT
label_git_path: Path to .git directory label_git_path: Път до директория .git
label_mercurial_path: Root directory label_mercurial_path: Коренна директория
label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee label_additional_workflow_transitions_for_assignee: Позволени са допълнителни преходи, когато потребителят е назначеният към задачата
button_expand_all: Expand all button_expand_all: Разгъване всички
button_collapse_all: Collapse all button_collapse_all: Скриване всички
label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author label_additional_workflow_transitions_for_author: Позволени са допълнителни преходи, когато потребителят е авторът
field_effective_date: Due date field_effective_date: Дата
text_default_encoding: "Default: UTF-8" text_default_encoding: "По подразбиране: UTF-8"
text_git_repo_example: a bare and local repository (e.g. /gitrepo, c:\gitrepo) text_git_repo_example: a bare and local repository (e.g. /gitrepo, c:\gitrepo)
label_notify_member_plural: Email issue updates label_notify_member_plural: Email issue updates
label_path_encoding: Path encoding label_path_encoding: Кодиране на пътищата
text_mercurial_repo_example: local repository (e.g. /hgrepo, c:\hgrepo) text_mercurial_repo_example: локално хранилище (например /hgrepo, c:\hgrepo)

View File

@ -1,3 +1,16 @@
#-- 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.
#++
Redmine::Activity.providers.values.flatten.uniq.collect(&:underscore).each {|klass| require_dependency klass } Redmine::Activity.providers.values.flatten.uniq.collect(&:underscore).each {|klass| require_dependency klass }
class UpdateJournalsForActsAsJournalized < ActiveRecord::Migration class UpdateJournalsForActsAsJournalized < ActiveRecord::Migration

View File

@ -1,3 +1,16 @@
#-- 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 BuildInitialJournalsForActsAsJournalized < ActiveRecord::Migration class BuildInitialJournalsForActsAsJournalized < ActiveRecord::Migration
def self.up def self.up
# This is provided here for migrating up after the JournalDetails has been removed # This is provided here for migrating up after the JournalDetails has been removed
@ -11,53 +24,24 @@ class BuildInitialJournalsForActsAsJournalized < ActiveRecord::Migration
klass.reset_column_information if klass.respond_to?(:reset_column_information) klass.reset_column_information if klass.respond_to?(:reset_column_information)
end 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
[Message, Attachment, Document, Changeset, Issue, TimeEntry, News].each do |p|
say_with_time("Building initial journals for #{p.class_name}") do say_with_time("Building initial journals for #{p.class_name}") do
# avoid touching the journaled object on journal creation
p.journal_class.class_exec {
def touch_journaled_after_creation
end
}
activity_type = p.activity_provider_options.keys.first activity_type = p.activity_provider_options.keys.first
# Create initial journals
p.find(:all).each do |o| 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 # Using rescue and save! here because either the Journal or the
# touched record could fail. This will catch either error and continue # touched record could fail. This will catch either error and continue
begin begin
new_journal.save! new_journal = o.recreate_initial_journal!
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 rescue ActiveRecord::RecordInvalid => ex
if new_journal.errors.count == 1 && new_journal.errors.first[0] == "version" 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. # Skip, only error was from creating the initial journal for a record that already had one.

View File

@ -1,3 +1,16 @@
#-- 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 AddChangesFromJournalDetailsForActsAsJournalized < ActiveRecord::Migration class AddChangesFromJournalDetailsForActsAsJournalized < ActiveRecord::Migration
def self.up def self.up
# This is provided here for migrating up after the JournalDetails has been removed # This is provided here for migrating up after the JournalDetails has been removed

View File

@ -18,6 +18,12 @@ class MergeWikiVersionsWithJournals < ActiveRecord::Migration
WikiContent.const_set("Version", Class.new(ActiveRecord::Base)) WikiContent.const_set("Version", Class.new(ActiveRecord::Base))
end end
# avoid touching WikiContent on journal creation
WikiContentJournal.class_exec {
def touch_journaled_after_creation
end
}
WikiContent::Version.find_by_sql("SELECT * FROM wiki_content_versions").each do |wv| 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, journal = WikiContentJournal.create!(:journaled_id => wv.wiki_content_id, :user_id => wv.author_id,
:notes => wv.comments, :created_at => wv.updated_on, :activity_type => "wiki_edits") :notes => wv.comments, :created_at => wv.updated_on, :activity_type => "wiki_edits")

View File

@ -0,0 +1,10 @@
class RemoveDoubleInitialWikiContentJournals < ActiveRecord::Migration
def self.up
# Remove the newest initial WikiContentJournal (the one erroneously created by a former migration) if there are more than one
WikiContentJournal.find(:all, :conditions => {:version => 1}).group_by(&:journaled_id).select {|k,v| v.size > 1}.each {|k,v| v.max_by(&:created_at).delete}
end
def self.down
# noop
end
end

View File

@ -1,6 +1,29 @@
= ChiliProject changelog = ChiliProject changelog
== TBD v2.0.0 == 2011-07-29 v2.1.0
* Bug #191: Add Next/Previous links to the top of search results
* Bug #467: uninitialized constant Journal::Journaled
* Bug #498: Wrong filters for int and float custom fields
* Bug #511: Encoding of strings coming out of SQLite
* Bug #512: reposman.rb do not work properly in Gentoo Linux.
* Bug #513: Attached files in "comment" no longer link to file
* Bug #514: Multiple emails for each forum post
* Bug #523: Gzipped history of wiki pages is garbeled during an update of an older version to 2.0
* Bug #530: Start date default should consider timezone
* Bug #536: CSRF Protection
* Bug #537: Accessing version of newly created WikiContent results in NoMethodError
* Bug #540: Hook helper_issues_show_detail_after_setting gets different parameters in Chili 1.x and 2.0
* Bug #542: Double initial journal for migrated wiki history
* Bug #543: Journalized touch on journal update causes StaleObjectErrors
* Bug #544: XSS in app/views/issues/show.rhtml
* Feature #499: Due date sort order should sort issues with no due date to the end of the list
* Feature #506: Support for "local" Gemfile - Gemfile.local
* Feature #526: Bulgarian translation
* Feature #539: Remove dead code in IssueHelper
* Task #518: Document how to create a Journal using acts_as_journalized
== 2011-07-01 v2.0.0
* Bug #262: Fix line endings * Bug #262: Fix line endings
* Bug #341: Remove English strings from RepositoriesHelper * Bug #341: Remove English strings from RepositoriesHelper

View File

@ -7,7 +7,7 @@ This is a sample plugin for Redmine
1. Copy the plugin directory into the vendor/plugins directory 1. Copy the plugin directory into the vendor/plugins directory
2. Migrate plugin: 2. Migrate plugin:
rake db:migrate_plugins rake db:migrate:plugins
3. Start Redmine 3. Start Redmine

View File

@ -12,7 +12,7 @@
#++ #++
# Sample plugin migration # Sample plugin migration
# Use rake db:migrate_plugins to migrate installed plugins # Use rake db:migrate:plugins to migrate installed plugins
class CreateMeetings < ActiveRecord::Migration class CreateMeetings < ActiveRecord::Migration
def self.up def self.up
create_table :meetings do |t| create_table :meetings do |t|

View File

@ -1,3 +1,4 @@
#!/usr/bin/env ruby
#-- copyright #-- copyright
# ChiliProject is a project management system. # ChiliProject is a project management system.
# #
@ -11,8 +12,6 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
#!/usr/bin/env ruby
# == Synopsis # == Synopsis
# #
# reposman: manages your repositories with Redmine # reposman: manages your repositories with Redmine

View File

@ -11,18 +11,14 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
namespace :db do module ChiliProject
desc 'Migrates installed plugins.' class Compatibility
task :migrate_plugins => :environment do # Is acts_as_journalized included?
if Rails.respond_to?('plugins') #
Rails.plugins.each do |plugin| # Released: ChiliProject 2.0.0
next unless plugin.respond_to?('migrate') def self.using_acts_as_journalized?
puts "Migrating #{plugin.name}..." Journal.included_modules.include?(Redmine::Acts::Journalized)
plugin.migrate
end
else
puts "Undefined method plugins for Rails!"
puts "Make sure engines plugin is installed."
end end
end end
end end

View File

@ -30,7 +30,7 @@ module ChiliProject
}) })
end end
# Get the raw namme of the currently used database adapter. # Get the raw name of the currently used database adapter.
# This string is set by the used adapter gem. # This string is set by the used adapter gem.
def self.adapter_name def self.adapter_name
ActiveRecord::Base.connection.adapter_name ActiveRecord::Base.connection.adapter_name

View File

@ -288,7 +288,12 @@ module Redmine
content = nil content = nil
scm_cmd(*cmd_args) { |io| io.binmode; content = io.read } scm_cmd(*cmd_args) { |io| io.binmode; content = io.read }
# git annotates binary files # git annotates binary files
return nil if content.is_binary_data? if content.respond_to?("is_binary_data?") && content.is_binary_data? # Ruby 1.8.x and <1.9.2
return nil
elsif content.respond_to?(:force_encoding) && (content.dup.force_encoding("UTF-8") != content.dup.force_encoding("BINARY")) # Ruby 1.9.2
# TODO: need to handle edge cases of non-binary content that isn't UTF-8
return nil
end
identifier = '' identifier = ''
# git shows commit author on the first occurrence only # git shows commit author on the first occurrence only
authors_by_commit = {} authors_by_commit = {}

View File

@ -16,7 +16,7 @@ require 'rexml/document'
module Redmine module Redmine
module VERSION #:nodoc: module VERSION #:nodoc:
MAJOR = 2 MAJOR = 2
MINOR = 0 MINOR = 1
PATCH = 0 PATCH = 0
TINY = PATCH # Redmine compat TINY = PATCH # Redmine compat

View File

@ -29,7 +29,7 @@ namespace :ci do
Rake::Task["db:drop"].invoke Rake::Task["db:drop"].invoke
Rake::Task["db:create"].invoke Rake::Task["db:create"].invoke
Rake::Task["db:migrate"].invoke Rake::Task["db:migrate"].invoke
Rake::Task["db:migrate_plugins"].invoke Rake::Task["db:migrate:plugins"].invoke
Rake::Task["db:schema:dump"].invoke Rake::Task["db:schema:dump"].invoke
Rake::Task["test:scm:update"].invoke Rake::Task["test:scm:update"].invoke
end end

View File

@ -20,3 +20,4 @@ end
deprecated_task :load_default_data, "redmine:load_default_data" deprecated_task :load_default_data, "redmine:load_default_data"
deprecated_task :migrate_from_mantis, "redmine:migrate_from_mantis" deprecated_task :migrate_from_mantis, "redmine:migrate_from_mantis"
deprecated_task :migrate_from_trac, "redmine:migrate_from_trac" deprecated_task :migrate_from_trac, "redmine:migrate_from_trac"
deprecated_task "db:migrate_plugins", "db:migrate:plugins"

View File

@ -19,11 +19,12 @@ begin
files << Dir['vendor/plugins/**/*.rb'].reject {|f| f.match(/test/) } # Exclude test files files << Dir['vendor/plugins/**/*.rb'].reject {|f| f.match(/test/) } # Exclude test files
t.files = files t.files = files
static_files = ['doc/CHANGELOG', static_files = ['doc/CHANGELOG.rdoc',
'doc/COPYING', 'doc/COPYING.rdoc',
'doc/INSTALL', 'doc/COPYRIGHT.rdoc',
'doc/RUNNING_TESTS', 'doc/INSTALL.rdoc',
'doc/UPGRADING'].join(',') 'doc/RUNNING_TESTS.rdoc',
'doc/UPGRADING.rdoc'].join(',')
t.options += ['--output-dir', './doc/app', '--files', static_files] t.options += ['--output-dir', './doc/app', '--files', static_files]
end end

View File

@ -331,6 +331,9 @@ dt.time-entry { background-image: url(../images/time.png); }
#search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); } #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
.search-pagination { text-align: center; }
.search-pagination a {padding: 0 5px; }
div#roadmap .related-issues { margin-bottom: 1em; } div#roadmap .related-issues { margin-bottom: 1em; }
div#roadmap .related-issues td.checkbox { display: none; } div#roadmap .related-issues td.checkbox { display: none; }
div#roadmap .wiki h1:first-child { display: none; } div#roadmap .wiki h1:first-child { display: none; }

View File

@ -12,7 +12,7 @@
#++ #++
class Journal < ActiveRecord::Base class Journal < ActiveRecord::Base
generator_for :journalized, :method => :generate_issue generator_for :journaled, :method => :generate_issue
generator_for :user, :method => :generate_user generator_for :user, :method => :generate_user
def self.generate_issue def self.generate_issue

View File

@ -61,6 +61,7 @@ class IssuesTest < ActionController::IntegrationTest
# add then remove 2 attachments to an issue # add then remove 2 attachments to an issue
def test_issue_attachments def test_issue_attachments
Issue.find(1).recreate_initial_journal!
log_user('jsmith', 'jsmith') log_user('jsmith', 'jsmith')
set_tmp_attachments_directory set_tmp_attachments_directory
@ -68,6 +69,7 @@ class IssuesTest < ActionController::IntegrationTest
:notes => 'Some notes', :notes => 'Some notes',
:attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'This is an attachment'}} :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'This is an attachment'}}
assert_redirected_to "/issues/1" assert_redirected_to "/issues/1"
follow_redirect!
# make sure attachment was saved # make sure attachment was saved
attachment = Issue.find(1).attachments.find_by_filename("testfile.txt") attachment = Issue.find(1).attachments.find_by_filename("testfile.txt")
@ -79,6 +81,12 @@ class IssuesTest < ActionController::IntegrationTest
# verify that the attachment was written to disk # verify that the attachment was written to disk
assert File.exist?(attachment.diskfile) assert File.exist?(attachment.diskfile)
assert_select "#history" do
assert_select ".journal .details" do
assert_select "a", :text => /testfile.txt/
end
end
# remove the attachments # remove the attachments
Issue.find(1).attachments.each(&:destroy) Issue.find(1).attachments.each(&:destroy)
assert_equal 0, Issue.find(1).attachments.length assert_equal 0, Issue.find(1).attachments.length

View File

@ -39,33 +39,30 @@ class IssuesHelperTest < HelperTestCase
@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
# TODO: Move test code to Journal class
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
@journal = IssueJournal.generate!(:changes => {"done_ratio" => [40, 100]}, :journaled => Issue.last) @journal = IssueJournal.generate!(:changes => {"done_ratio" => [40, 100]}, :journaled => Issue.last)
assert_equal "% Done changed from 40 to 100", show_detail(@journal, @journal.details.to_a.first, true) assert_equal "% Done changed from 40 to 100", @journal.render_detail(@journal.details.to_a.first, true)
end end
should 'show a new attribute' do should 'show a new attribute' do
@journal = IssueJournal.generate!(:changes => {"done_ratio" => [nil, 100]}, :journaled => Issue.last) @journal = IssueJournal.generate!(:changes => {"done_ratio" => [nil, 100]}, :journaled => Issue.last)
assert_equal "% Done set to 100", show_detail(@journal, @journal.details.to_a.first, true) assert_equal "% Done set to 100", @journal.render_detail(@journal.details.to_a.first, true)
end end
should 'show a deleted attribute' do should 'show a deleted attribute' do
@journal = IssueJournal.generate!(:changes => {"done_ratio" => [50, nil]}, :journaled => Issue.last) @journal = IssueJournal.generate!(:changes => {"done_ratio" => [50, nil]}, :journaled => Issue.last)
assert_equal "% Done deleted (50)", show_detail(@journal, @journal.details.to_a.first, true) assert_equal "% Done deleted (50)", @journal.render_detail(@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
@journal = IssueJournal.generate!(:changes => {"done_ratio" => [40, 100]}, :journaled => Issue.last) @journal = IssueJournal.generate!(:changes => {"done_ratio" => [40, 100]}, :journaled => Issue.last)
@response.body = show_detail(@journal, @journal.details.to_a.first, false) @response.body = @journal.render_detail(@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'
@ -74,7 +71,7 @@ class IssuesHelperTest < HelperTestCase
should 'show a new attribute with HTML highlights' do should 'show a new attribute with HTML highlights' do
@journal = IssueJournal.generate!(:changes => {"done_ratio" => [nil, 100]}, :journaled => Issue.last) @journal = IssueJournal.generate!(:changes => {"done_ratio" => [nil, 100]}, :journaled => Issue.last)
@response.body = show_detail(@journal, @journal.details.to_a.first, false) @response.body = @journal.render_detail(@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'
@ -82,7 +79,7 @@ class IssuesHelperTest < HelperTestCase
should 'show a deleted attribute with HTML highlights' do should 'show a deleted attribute with HTML highlights' do
@journal = IssueJournal.generate!(:changes => {"done_ratio" => [50, nil]}, :journaled => Issue.last) @journal = IssueJournal.generate!(:changes => {"done_ratio" => [50, nil]}, :journaled => Issue.last)
@response.body = show_detail(@journal, @journal.details.to_a.first, false) @response.body = @journal.render_detail(@journal.details.to_a.first, false)
assert_select 'strong', :text => '% Done' assert_select 'strong', :text => '% Done'
assert_select 'strike' do assert_select 'strike' do
@ -94,24 +91,24 @@ 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
@journal = IssueJournal.generate!(:changes => {"start_date" => ['2010-01-01', '2010-01-31']}, :journaled => Issue.last) @journal = IssueJournal.generate!(:changes => {"start_date" => ['2010-01-01', '2010-01-31']}, :journaled => Issue.last)
assert_match "01/31/2010", show_detail(@journal, @journal.details.to_a.first, true) assert_match "01/31/2010", @journal.render_detail(@journal.details.to_a.first, true)
end end
should "format the old date" do should "format the old date" do
@journal = IssueJournal.generate!(:changes => {"start_date" => ['2010-01-01', '2010-01-31']}, :journaled => Issue.last) @journal = IssueJournal.generate!(:changes => {"start_date" => ['2010-01-01', '2010-01-31']}, :journaled => Issue.last)
assert_match "01/01/2010", show_detail(@journal, @journal.details.to_a.first, true) assert_match "01/01/2010", @journal.render_detail(@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
@journal = IssueJournal.generate!(:changes => {"due_date" => ['2010-01-01', '2010-01-31']}, :journaled => Issue.last) @journal = IssueJournal.generate!(:changes => {"due_date" => ['2010-01-01', '2010-01-31']}, :journaled => Issue.last)
assert_match "01/31/2010", show_detail(@journal, @journal.details.to_a.first, true) assert_match "01/31/2010", @journal.render_detail(@journal.details.to_a.first, true)
end end
should "format the old date" do should "format the old date" do
@journal = IssueJournal.generate!(:changes => {"due_date" => ['2010-01-01', '2010-01-31']}, :journaled => Issue.last) @journal = IssueJournal.generate!(:changes => {"due_date" => ['2010-01-01', '2010-01-31']}, :journaled => Issue.last)
assert_match "01/01/2010", show_detail(@journal, @journal.details.to_a.first, true) assert_match "01/01/2010", @journal.render_detail(@journal.details.to_a.first, true)
end end
end end

View File

@ -13,7 +13,7 @@
require File.expand_path('../../test_helper', __FILE__) require File.expand_path('../../test_helper', __FILE__)
class JournalTest < ActiveSupport::TestCase class JournalTest < ActiveSupport::TestCase
fixtures :issues, :issue_statuses, :journals fixtures :issues, :issue_statuses, :journals, :enumerations
def setup def setup
@journal = IssueJournal.find(1) @journal = IssueJournal.find(1)
@ -94,4 +94,24 @@ class JournalTest < ActiveSupport::TestCase
assert_not_equal start, @issue.reload.updated_on assert_not_equal start, @issue.reload.updated_on
end end
test "accessing #journaled on a Journal should not error (parent class)" do
journal = Journal.new
assert_nothing_raised do
assert_equal nil, journal.journaled
end
end
test "setting journal fields through the journaled object for creation" do
@issue = Issue.generate_for_project!(Project.generate!)
@issue.journal_user = @issue.author
@issue.journal_notes = 'Test setting fields on Journal from Issue'
assert_difference('Journal.count') do
assert @issue.save
end
assert_equal "Test setting fields on Journal from Issue", @issue.last_journal.notes
assert_equal @issue.author, @issue.last_journal.user
end
end end

View File

@ -155,7 +155,8 @@ begin
assert_equal "2010-09-18 19:59:46".to_time, last_rev.time assert_equal "2010-09-18 19:59:46".to_time, last_rev.time
end end
def test_latin_1_path # TODO: need to handle edge cases of non-binary content that isn't UTF-8
should_eventually "test_latin_1_path" do
if WINDOWS_PASS if WINDOWS_PASS
# #
else else
@ -163,7 +164,9 @@ begin
['4fc55c43bf3d3dc2efb66145365ddc17639ce81e', '4fc55c43bf3'].each do |r1| ['4fc55c43bf3d3dc2efb66145365ddc17639ce81e', '4fc55c43bf3'].each do |r1|
assert @adapter.diff(p2, r1) assert @adapter.diff(p2, r1)
assert @adapter.cat(p2, r1) assert @adapter.cat(p2, r1)
assert_equal 1, @adapter.annotate(p2, r1).lines.length annotation = @adapter.annotate(p2, r1)
assert annotation.present?, "No annotation returned"
assert_equal 1, annotation.lines.length
['64f1f3e89ad1cb57976ff0ad99a107012ba3481d', '64f1f3e89ad1cb5797'].each do |r2| ['64f1f3e89ad1cb57976ff0ad99a107012ba3481d', '64f1f3e89ad1cb5797'].each do |r2|
assert @adapter.diff(p2, r1, r2) assert @adapter.diff(p2, r1, r2)
end end

View File

@ -179,7 +179,7 @@ class MailerTest < ActiveSupport::TestCase
assert_nil mail.references assert_nil mail.references
assert_select_email do assert_select_email do
# link to the message # link to the message
assert_select "a[href=?]", "http://mydomain.foo/boards/#{message.board.id}/topics/#{message.id}", :text => message.subject assert_select "a[href*=?]", "http://mydomain.foo/boards/#{message.board.id}/topics/#{message.id}", :text => message.subject
end end
end end
@ -316,7 +316,7 @@ class MailerTest < ActiveSupport::TestCase
end end
def test_wiki_content_added def test_wiki_content_added
content = WikiContent.find(:first) content = WikiContent.find(1)
valid_languages.each do |lang| valid_languages.each do |lang|
Setting.default_language = lang.to_s Setting.default_language = lang.to_s
assert_difference 'ActionMailer::Base.deliveries.size' do assert_difference 'ActionMailer::Base.deliveries.size' do
@ -326,7 +326,7 @@ class MailerTest < ActiveSupport::TestCase
end end
def test_wiki_content_updated def test_wiki_content_updated
content = WikiContent.find(:first) content = WikiContent.find(1)
valid_languages.each do |lang| valid_languages.each do |lang|
Setting.default_language = lang.to_s Setting.default_language = lang.to_s
assert_difference 'ActionMailer::Base.deliveries.size' do assert_difference 'ActionMailer::Base.deliveries.size' do

View File

@ -16,6 +16,7 @@ class MessageTest < ActiveSupport::TestCase
fixtures :projects, :roles, :members, :member_roles, :boards, :messages, :users, :watchers fixtures :projects, :roles, :members, :member_roles, :boards, :messages, :users, :watchers
def setup def setup
Setting.notified_events = ['message_posted']
@board = Board.find(1) @board = Board.find(1)
@user = User.find(1) @user = User.find(1)
end end
@ -138,4 +139,12 @@ class MessageTest < ActiveSupport::TestCase
message.sticky = '1' message.sticky = '1'
assert_equal 1, message.sticky assert_equal 1, message.sticky
end end
test "email notifications for creating a message" do
assert_difference("ActionMailer::Base.deliveries.count") do
message = Message.new(:board => @board, :subject => 'Test message', :content => 'Test message content', :author => @user)
assert message.save
end
end
end end

View File

@ -80,4 +80,11 @@ class WikiContentTest < ActiveSupport::TestCase
page.reload page.reload
assert_equal 500.kilobyte, page.content.text.size assert_equal 500.kilobyte, page.content.text.size
end end
test "new WikiContent is version 0" do
page = WikiPage.new(:wiki => @wiki, :title => "Page")
page.content = WikiContent.new(:text => "Content text", :author => User.find(1), :comments => "My comment")
assert_equal 0, page.content.version
end
end end

View File

@ -19,7 +19,7 @@
Dir[File.expand_path("../redmine/acts/journalized/*.rb", __FILE__)].each{|f| require f } Dir[File.expand_path("../redmine/acts/journalized/*.rb", __FILE__)].each{|f| require f }
require_dependency File.expand_path('lib/ar_condition', Rails.root) require "ar_condition"
module Redmine module Redmine
module Acts module Acts
@ -140,7 +140,7 @@ module Redmine
h[:find_options] ||= {} # in case it is nil h[:find_options] ||= {} # in case it is nil
h[:find_options] = {}.tap do |opts| h[:find_options] = {}.tap do |opts|
cond = ARCondition.new cond = ::ARCondition.new
cond.add(["#{journal_class.table_name}.activity_type = ?", h[:type]]) cond.add(["#{journal_class.table_name}.activity_type = ?", h[:type]])
cond.add(h[:find_options][:conditions]) if h[:find_options][:conditions] cond.add(h[:find_options][:conditions]) if h[:find_options][:conditions]
opts[:conditions] = cond.conditions opts[:conditions] = cond.conditions

View File

@ -0,0 +1,9 @@
class JournalDetail
attr_reader :prop_key, :value, :old_value
def initialize(prop_key, old_value, value)
@prop_key = prop_key
@old_value = old_value
@value = value
end
end

View File

@ -27,8 +27,18 @@ module JournalFormatter
include CustomFieldsHelper include CustomFieldsHelper
include ActionView::Helpers::TagHelper include ActionView::Helpers::TagHelper
include ActionView::Helpers::UrlHelper include ActionView::Helpers::UrlHelper
include ActionController::UrlWriter
extend Redmine::I18n extend Redmine::I18n
def self.included(base)
base.class_eval do
# Required to use any link_to in the formatters
def self.default_url_options
{:only_path => true }
end
end
end
def self.register(hash) def self.register(hash)
if hash[:class] if hash[:class]
klazz = hash.delete(:class) klazz = hash.delete(:class)
@ -90,9 +100,7 @@ module JournalFormatter
def format_html_attachment_detail(key, value) def format_html_attachment_detail(key, value)
if !value.blank? && a = Attachment.find_by_id(key.to_i) if !value.blank? && a = Attachment.find_by_id(key.to_i)
# Link to the attachment if it has not been removed link_to_attachment(a)
# FIXME: this is broken => link_to_attachment(a)
a.filename
else else
content_tag("i", h(value)) if value.present? content_tag("i", h(value)) if value.present?
end end
@ -163,7 +171,7 @@ module JournalFormatter
end end
label, old_value, value = attr_detail || cv_detail || attachment_detail label, old_value, value = attr_detail || cv_detail || attachment_detail
Redmine::Hook.call_hook :helper_issues_show_detail_after_setting, {:detail => detail, Redmine::Hook.call_hook :helper_issues_show_detail_after_setting, {:detail => JournalDetail.new(label, old_value, value),
:label => label, :value => value, :old_value => old_value } :label => label, :value => value, :old_value => old_value }
return nil unless label || old_value || value # print nothing if there are no values 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) label, old_value, value = [label, old_value, value].collect(&:to_s)

View File

@ -68,6 +68,48 @@ module Redmine::Acts::Journalized
# Instance methods that determine whether to save a journal and actually perform the save. # Instance methods that determine whether to save a journal and actually perform the save.
module InstanceMethods module InstanceMethods
# Recreates the initial journal used to track the beginning state
# of the object. Useful for objects that didn't have an initial journal
# created (e.g. legacy data)
def recreate_initial_journal!
new_journal = journals.find_by_version(1)
new_journal ||= journals.build
# Mock up a list of changes for the creation journal based on Class defaults
new_attributes = self.class.new.attributes.except(self.class.primary_key,
self.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, self.send(name)] # [initial_value, creation_value]
end
new_journal.changes = creation_changes
new_journal.version = 1
new_journal.activity_type = self.class.send(:journalized_activity_hash, {})[:type]
if respond_to?(:author)
new_journal.user = author
elsif respond_to?(:user)
new_journal.user = user
end
new_journal.save!
new_journal.reload
# Backdate journal
if respond_to?(:created_at)
new_journal.update_attribute(:created_at, created_at)
elsif respond_to?(:created_on)
new_journal.update_attribute(:created_at, created_on)
end
new_journal
end
private private
# Returns whether a new journal should be created upon updating the parent record. # Returns whether a new journal should be created upon updating the parent record.
# A new journal will be created if # A new journal will be created if
@ -120,7 +162,8 @@ module Redmine::Acts::Journalized
def journal_attributes def journal_attributes
attributes = { :journaled_id => self.id, :activity_type => activity_type, attributes = { :journaled_id => self.id, :activity_type => activity_type,
:changes => journal_changes, :version => last_version + 1, :changes => journal_changes, :version => last_version + 1,
:notes => journal_notes, :user_id => (journal_user.try(:id) || User.current.try(:id)) } :notes => journal_notes, :user_id => (journal_user.try(:id) || User.current.try(:id))
}.merge(extra_journal_attributes || {})
end end
end end
end end

View File

@ -28,7 +28,7 @@ module Redmine::Acts::Journalized
before_save :init_journal before_save :init_journal
after_save :reset_instance_variables after_save :reset_instance_variables
attr_reader :journal_notes, :journal_user attr_accessor :journal_notes, :journal_user, :extra_journal_attributes
end end
end end