Compare commits

...

18 Commits
0.8.0 ... 0.8.1

Author SHA1 Message Date
Jean-Philippe Lang
ed6151b4d2 tagged version 0.8.1
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/tags/0.8.1@2471 e93f8b46-1217-0410-a6f0-8f06a7374b81
2009-02-15 09:27:21 +00:00
Jean-Philippe Lang
3d1bd79ffb Set version to 0.8.1
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.8-stable@2470 e93f8b46-1217-0410-a6f0-8f06a7374b81
2009-02-15 09:08:26 +00:00
Jean-Philippe Lang
7f957653ad Merged r2463, r2465, r2467, r2468 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.8-stable@2469 e93f8b46-1217-0410-a6f0-8f06a7374b81
2009-02-15 09:04:24 +00:00
Jean-Philippe Lang
9a9141041e Merged r2461 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.8-stable@2462 e93f8b46-1217-0410-a6f0-8f06a7374b81
2009-02-13 17:32:27 +00:00
Jean-Philippe Lang
bc4249e3d3 Merged r2270, r2344, r2359, r2360, r2362, r2363, r2415, r2423, r2424 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.8-stable@2425 e93f8b46-1217-0410-a6f0-8f06a7374b81
2009-02-09 18:46:00 +00:00
Jean-Philippe Lang
fae04f3ae1 Merged r2246 and r2248 from trunk (#2456).
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.8-stable@2310 e93f8b46-1217-0410-a6f0-8f06a7374b81
2009-01-24 13:01:31 +00:00
Jean-Philippe Lang
b13ef64794 Merged r2265 from trunk (#2523).
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.8-stable@2309 e93f8b46-1217-0410-a6f0-8f06a7374b81
2009-01-24 12:27:56 +00:00
Jean-Philippe Lang
683e1c5d73 Merged r2266 from trunk (#2522).
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.8-stable@2308 e93f8b46-1217-0410-a6f0-8f06a7374b81
2009-01-24 12:13:16 +00:00
Jean-Philippe Lang
1c1755d278 Merged r2280 from trunk (#2506).
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.8-stable@2307 e93f8b46-1217-0410-a6f0-8f06a7374b81
2009-01-24 12:11:52 +00:00
Jean-Philippe Lang
5e6ff86f47 Merged r2302 from trunk (#2466).
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.8-stable@2306 e93f8b46-1217-0410-a6f0-8f06a7374b81
2009-01-24 12:10:24 +00:00
Jean-Philippe Lang
764393aa6a Merged r2250 and r2251 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.8-stable@2252 e93f8b46-1217-0410-a6f0-8f06a7374b81
2009-01-10 10:51:59 +00:00
Jean-Philippe Lang
3155b8ccad Merged r2226 and r2233 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.8-stable@2245 e93f8b46-1217-0410-a6f0-8f06a7374b81
2009-01-05 18:16:14 +00:00
Jean-Philippe Lang
567c8ed9b0 Fixed: Darcs adapter works on Win32 only (wrong quotes in commands #2402).
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.8-stable@2231 e93f8b46-1217-0410-a6f0-8f06a7374b81
2009-01-04 13:24:56 +00:00
Jean-Philippe Lang
49b6f9e4dd Merged r2218 to r2225 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.8-stable@2230 e93f8b46-1217-0410-a6f0-8f06a7374b81
2009-01-04 13:10:16 +00:00
Jean-Philippe Lang
c9d4d3a2be Merged r2209 to r2211 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.8-stable@2229 e93f8b46-1217-0410-a6f0-8f06a7374b81
2009-01-04 13:03:34 +00:00
Jean-Philippe Lang
31178553f3 Merged r2116, r2117 and r2187 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.8-stable@2217 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-12-31 11:40:03 +00:00
Jean-Philippe Lang
dea10c54f9 Merged r2164 to r2167, r2188 and r2189 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.8-stable@2216 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-12-31 11:31:02 +00:00
Jean-Philippe Lang
6abd32be9e Merged r2212 to r2214 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.8-stable@2215 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-12-31 10:47:19 +00:00
114 changed files with 1353 additions and 788 deletions

View File

@@ -86,6 +86,7 @@ class AdminController < ApplicationController
@flags = {
:default_admin_changed => User.find(:first, :conditions => ["login=? and hashed_password=?", 'admin', User.hash_password('admin')]).nil?,
:file_repository_writable => File.writable?(Attachment.storage_path),
:plugin_assets_writable => File.writable?(Engines.public_directory),
:rmagick_available => Object.const_defined?(:Magick)
}
end

View File

@@ -175,6 +175,7 @@ class ApplicationController < ActionController::Base
# TODO: move to model
def attach_files(obj, attachments)
attached = []
unsaved = []
if attachments && attachments.is_a?(Hash)
attachments.each_value do |attachment|
file = attachment['file']
@@ -183,7 +184,10 @@ class ApplicationController < ActionController::Base
:file => file,
:description => attachment['description'].to_s.strip,
:author => User.current)
attached << a unless a.new_record?
a.new_record? ? (unsaved << a) : (attached << a)
end
if unsaved.any?
flash[:warning] = l(:warning_attachments_not_saved, unsaved.size)
end
end
attached

View File

@@ -1,5 +1,5 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
# Redmine - project management software
# Copyright (C) 2006-2008 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -17,7 +17,11 @@
class AttachmentsController < ApplicationController
before_filter :find_project
before_filter :read_authorize, :except => :destroy
before_filter :delete_authorize, :only => :destroy
verify :method => :post, :only => :destroy
def show
if @attachment.is_diff?
@diff = File.new(@attachment.diskfile, "rb").read
@@ -31,25 +35,40 @@ class AttachmentsController < ApplicationController
end
def download
@attachment.increment_download if @attachment.container.is_a?(Version)
if @attachment.container.is_a?(Version) || @attachment.container.is_a?(Project)
@attachment.increment_download
end
# images are sent inline
send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
:type => @attachment.content_type,
:disposition => (@attachment.image? ? 'inline' : 'attachment')
end
def destroy
# Make sure association callbacks are called
@attachment.container.attachments.delete(@attachment)
redirect_to :back
rescue ::ActionController::RedirectBackError
redirect_to :controller => 'projects', :action => 'show', :id => @project
end
private
def find_project
@attachment = Attachment.find(params[:id])
# Show 404 if the filename in the url is wrong
raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename
@project = @attachment.project
permission = @attachment.container.is_a?(Version) ? :view_files : "view_#{@attachment.container.class.name.underscore.pluralize}".to_sym
allowed = User.current.allowed_to?(permission, @project)
allowed ? true : (User.current.logged? ? render_403 : require_login)
rescue ActiveRecord::RecordNotFound
render_404
end
def read_authorize
@attachment.visible? ? true : deny_access
end
def delete_authorize
@attachment.deletable? ? true : deny_access
end
end

View File

@@ -71,11 +71,6 @@ class DocumentsController < ApplicationController
Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('document_added')
redirect_to :action => 'show', :id => @document
end
def destroy_attachment
@document.attachments.find(params[:attachment_id]).destroy
redirect_to :action => 'show', :id => @document
end
private
def find_project

View File

@@ -18,7 +18,7 @@
class IssuesController < ApplicationController
menu_item :new_issue, :only => :new
before_filter :find_issue, :only => [:show, :edit, :reply, :destroy_attachment]
before_filter :find_issue, :only => [:show, :edit, :reply]
before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
before_filter :find_project, :only => [:new, :update_form, :preview]
before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu]
@@ -30,8 +30,6 @@ class IssuesController < ApplicationController
include ProjectsHelper
helper :custom_fields
include CustomFieldsHelper
helper :ifpdf
include IfpdfHelper
helper :issue_relations
include IssueRelationsHelper
helper :watchers
@@ -43,6 +41,7 @@ class IssuesController < ApplicationController
include SortHelper
include IssuesHelper
helper :timelog
include Redmine::Export::PDF
def index
retrieve_query
@@ -68,7 +67,7 @@ class IssuesController < ApplicationController
format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
format.pdf { send_data(render(:template => 'issues/index.rfpdf', :layout => false), :type => 'application/pdf', :filename => 'export.pdf') }
format.pdf { send_data(issues_to_pdf(@issues, @project), :type => 'application/pdf', :filename => 'export.pdf') }
end
else
# Send html if the query is not valid
@@ -106,7 +105,7 @@ class IssuesController < ApplicationController
respond_to do |format|
format.html { render :template => 'issues/show.rhtml' }
format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
format.pdf { send_data(render(:template => 'issues/show.rfpdf', :layout => false), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
end
end
@@ -123,7 +122,10 @@ class IssuesController < ApplicationController
render :nothing => true, :layout => true
return
end
@issue.attributes = params[:issue]
if params[:issue].is_a?(Hash)
@issue.attributes = params[:issue]
@issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
end
@issue.author = User.current
default_status = IssueStatus.default
@@ -145,7 +147,8 @@ class IssuesController < ApplicationController
attach_files(@issue, params[:attachments])
flash[:notice] = l(:notice_successful_create)
Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
redirect_to :controller => 'issues', :action => 'show', :id => @issue
redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
{ :action => 'show', :id => @issue })
return
end
end
@@ -183,7 +186,7 @@ class IssuesController < ApplicationController
if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
# Log spend time
if current_role.allowed_to?(:log_time)
if User.current.allowed_to?(:log_time, @project)
@time_entry.save
end
if !journal.new_record?
@@ -315,17 +318,6 @@ class IssuesController < ApplicationController
@issues.each(&:destroy)
redirect_to :action => 'index', :project_id => @project
end
def destroy_attachment
a = @issue.attachments.find(params[:attachment_id])
a.destroy
journal = @issue.init_journal(User.current)
journal.details << JournalDetail.new(:property => 'attachment',
:prop_key => a.id,
:old_value => a.filename)
journal.save
redirect_to :action => 'show', :id => @issue
end
def gantt
@gantt = Redmine::Helpers::Gantt.new(params)
@@ -354,7 +346,7 @@ class IssuesController < ApplicationController
respond_to do |format|
format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.identifier}-gantt.png") } if @gantt.respond_to?('to_image')
format.pdf { send_data(render(:template => "issues/gantt.rfpdf", :layout => false), :type => 'application/pdf', :filename => "#{@project.nil? ? '' : "#{@project.identifier}-" }gantt.pdf") }
format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{@project.nil? ? '' : "#{@project.identifier}-" }gantt.pdf") }
end
end

View File

@@ -33,8 +33,6 @@ class ProjectsController < ApplicationController
include SortHelper
helper :custom_fields
include CustomFieldsHelper
helper :ifpdf
include IfpdfHelper
helper :issues
helper IssuesHelper
helper :queries
@@ -84,6 +82,11 @@ class ProjectsController < ApplicationController
# Show @project
def show
if params[:jump]
# try to redirect to the requested menu item
redirect_to_project_menu_item(@project, params[:jump]) && return
end
@members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role}
@subprojects = @project.children.find(:all, :conditions => Project.visible_by(User.current))
@news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
@@ -188,10 +191,13 @@ class ProjectsController < ApplicationController
def add_file
if request.post?
@version = @project.versions.find_by_id(params[:version_id])
attachments = attach_files(@version, params[:attachments])
Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('file_added')
container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
attachments = attach_files(container, params[:attachments])
if !attachments.empty? && Setting.notified_events.include?('file_added')
Mailer.deliver_attachments_added(attachments)
end
redirect_to :controller => 'projects', :action => 'list_files', :id => @project
return
end
@versions = @project.versions.sort
end
@@ -203,7 +209,8 @@ class ProjectsController < ApplicationController
'size' => "#{Attachment.table_name}.filesize",
'downloads' => "#{Attachment.table_name}.downloads"
@versions = @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
@containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
@containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
render :layout => !request.xhr?
end

View File

@@ -127,6 +127,9 @@ class RepositoriesController < ApplicationController
end
def annotate
@entry = @repository.entry(@path, @rev)
show_error_not_found and return unless @entry
@annotate = @repository.scm.annotate(@path, @rev)
render_error l(:error_scm_annotate) and return if @annotate.nil? || @annotate.empty?
end

View File

@@ -40,7 +40,7 @@ class SettingsController < ApplicationController
@options[:user_format] = User::USER_FORMATS.keys.collect {|f| [User.current.name(f), f.to_s] }
@deliveries = ActionMailer::Base.perform_deliveries
@guessed_host_and_path = request.host_with_port
@guessed_host_and_path = request.host_with_port.dup
@guessed_host_and_path << ('/'+ request.relative_url_root.gsub(%r{^\/}, '')) unless request.relative_url_root.blank?
end

View File

@@ -37,12 +37,6 @@ class VersionsController < ApplicationController
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
end
def destroy_file
@version.attachments.find(params[:attachment_id]).destroy
flash[:notice] = l(:notice_successful_delete)
redirect_to :controller => 'projects', :action => 'list_files', :id => @project
end
def status_by
respond_to do |format|
format.html { render :action => 'show' }

View File

@@ -21,7 +21,7 @@ class WikiController < ApplicationController
before_filter :find_wiki, :authorize
before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy]
verify :method => :post, :only => [:destroy, :destroy_attachment, :protect], :redirect_to => { :action => :index }
verify :method => :post, :only => [:destroy, :protect], :redirect_to => { :action => :index }
helper :attachments
include AttachmentsHelper
@@ -181,13 +181,6 @@ class WikiController < ApplicationController
redirect_to :action => 'index', :page => @page.title
end
def destroy_attachment
@page = @wiki.find_page(params[:page])
return render_403 unless editable?
@page.attachments.find(params[:attachment_id]).destroy
redirect_to :action => 'index', :page => @page.title
end
private
def find_wiki

View File

@@ -147,6 +147,15 @@ module ApplicationHelper
end
content
end
# Renders flash messages
def render_flash_messages
s = ''
flash.each do |k,v|
s << content_tag('div', v, :class => "flash #{k}")
end
s
end
# Truncates and returns the string as a single line
def truncate_single_line(string, *args)
@@ -282,16 +291,15 @@ module ApplicationHelper
attachments = attachments.sort_by(&:created_on).reverse
text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(bmp|gif|jpg|jpeg|png))!/i) do |m|
style = $1
filename = $6
rf = Regexp.new(Regexp.escape(filename), Regexp::IGNORECASE)
filename = $6.downcase
# search for the picture in attachments
if found = attachments.detect { |att| att.filename =~ rf }
if found = attachments.detect { |att| att.filename.downcase == filename }
image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1")
alt = desc.blank? ? nil : "(#{desc})"
"!#{style}#{image_url}#{alt}!"
else
"!#{style}#{filename}!"
m
end
end
end
@@ -526,6 +534,7 @@ module ApplicationHelper
def back_url_hidden_field_tag
back_url = params[:back_url] || request.env['HTTP_REFERER']
back_url = CGI.unescape(back_url.to_s)
hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
end

View File

@@ -16,10 +16,15 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module AttachmentsHelper
# displays the links to a collection of attachments
def link_to_attachments(attachments, options = {})
if attachments.any?
render :partial => 'attachments/links', :locals => {:attachments => attachments, :options => options}
# Displays view/delete links to the attachments of the given object
# Options:
# :author -- author names are not displayed if set to false
def link_to_attachments(container, options = {})
options.assert_valid_keys(:author)
if container.attachments.any?
options = {:deletable => container.attachments_deletable?, :author => true}.merge(options)
render :partial => 'attachments/links', :locals => {:attachments => container.attachments, :options => options}
end
end

View File

@@ -1,85 +0,0 @@
# redMine - project management software
# Copyright (C) 2006 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'iconv'
require 'rfpdf/chinese'
module IfpdfHelper
class IFPDF < FPDF
include GLoc
attr_accessor :footer_date
def initialize(lang)
super()
set_language_if_valid lang
case current_language.to_s
when 'ja'
extend(PDF_Japanese)
AddSJISFont()
@font_for_content = 'SJIS'
@font_for_footer = 'SJIS'
when 'zh'
extend(PDF_Chinese)
AddGBFont()
@font_for_content = 'GB'
@font_for_footer = 'GB'
when 'zh-tw'
extend(PDF_Chinese)
AddBig5Font()
@font_for_content = 'Big5'
@font_for_footer = 'Big5'
else
@font_for_content = 'Arial'
@font_for_footer = 'Helvetica'
end
SetCreator(Redmine::Info.app_name)
SetFont(@font_for_content)
end
def SetFontStyle(style, size)
SetFont(@font_for_content, style, size)
end
def Cell(w,h=0,txt='',border=0,ln=0,align='',fill=0,link='')
@ic ||= Iconv.new(l(:general_pdf_encoding), 'UTF-8')
# these quotation marks are not correctly rendered in the pdf
txt = txt.gsub(/[“”]/, '"') if txt
txt = begin
# 0x5c char handling
txtar = txt.split('\\')
txtar << '' if txt[-1] == ?\\
txtar.collect {|x| @ic.iconv(x)}.join('\\').gsub(/\\/, "\\\\\\\\")
rescue
txt
end || ''
super w,h,txt,border,ln,align,fill,link
end
def Footer
SetFont(@font_for_footer, 'I', 8)
SetY(-15)
SetX(15)
Cell(0, 5, @footer_date, 0, 0, 'L')
SetY(-15)
SetX(-30)
Cell(0, 5, PageNo().to_s + '/{nb}', 0, 0, 'C')
end
end
end

View File

@@ -36,6 +36,7 @@ module IssuesHelper
# Returns a string of css classes that apply to the given issue
def css_issue_classes(issue)
s = "issue status-#{issue.status.position} priority-#{issue.priority.position}"
s << ' closed' if issue.closed?
s << ' overdue' if issue.overdue?
s
end

View File

@@ -16,6 +16,8 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module TimelogHelper
include ApplicationHelper
def render_timelog_breadcrumb
links = []
links << link_to(l(:label_project_all), {:project_id => nil, :issue_id => nil})
@@ -81,7 +83,7 @@ module TimelogHelper
csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
# csv lines
entries.each do |entry|
fields = [l_date(entry.spent_on),
fields = [format_date(entry.spent_on),
entry.user,
entry.activity,
entry.project,

View File

@@ -98,6 +98,14 @@ class Attachment < ActiveRecord::Base
container.project
end
def visible?(user=User.current)
container.attachments_visible?(user)
end
def deletable?(user=User.current)
container.attachments_deletable?(user)
end
def image?
self.filename =~ /\.(jpe?g|gif|png)$/i
end

View File

@@ -18,7 +18,7 @@
class Document < ActiveRecord::Base
belongs_to :project
belongs_to :category, :class_name => "Enumeration", :foreign_key => "category_id"
has_many :attachments, :as => :container, :dependent => :destroy
acts_as_attachable :delete_permission => :manage_documents
acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},

View File

@@ -26,13 +26,13 @@ class Issue < ActiveRecord::Base
belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
has_many :journals, :as => :journalized, :dependent => :destroy
has_many :attachments, :as => :container, :dependent => :destroy
has_many :time_entries, :dependent => :delete_all
has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
acts_as_attachable :after_remove => :attachment_removed
acts_as_customizable
acts_as_watchable
acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
@@ -45,7 +45,7 @@ class Issue < ActiveRecord::Base
acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
:author_key => :author_id
validates_presence_of :subject, :description, :priority, :project, :tracker, :author, :status
validates_presence_of :subject, :priority, :project, :tracker, :author, :status
validates_length_of :subject, :maximum => 255
validates_inclusion_of :done_ratio, :in => 0..100
validates_numericality_of :estimated_hours, :allow_nil => true
@@ -266,4 +266,15 @@ class Issue < ActiveRecord::Base
def to_s
"#{tracker} ##{id}: #{subject}"
end
private
# Callback on attachment deletion
def attachment_removed(obj)
journal = init_journal(User.current)
journal.details << JournalDetail.new(:property => 'attachment',
:prop_key => obj.id,
:old_value => obj.filename)
journal.save
end
end

View File

@@ -90,6 +90,13 @@ class MailHandler < ActionMailer::Base
end
issue.subject = email.subject.chomp.toutf8
issue.description = plain_text_body
# custom fields
issue.custom_field_values = issue.available_custom_fields.inject({}) do |h, c|
if value = get_keyword(c.name, :override => true)
h[c.id] = value
end
h
end
issue.save!
add_attachments(issue)
logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
@@ -155,8 +162,8 @@ class MailHandler < ActionMailer::Base
end
end
def get_keyword(attr)
if @@handler_options[:allow_override].include?(attr.to_s) && plain_text_body =~ /^#{attr}:[ \t]*(.+)$/i
def get_keyword(attr, options={})
if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && plain_text_body =~ /^#{attr}:[ \t]*(.+)$/i
$1.strip
elsif !@@handler_options[:issue][attr].blank?
@@handler_options[:issue][attr]

View File

@@ -28,6 +28,7 @@ class Mailer < ActionMailer::Base
'Issue-Author' => issue.author.login
redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
recipients issue.recipients
cc(issue.watcher_recipients - @recipients)
subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
body :issue => issue,
:issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
@@ -39,6 +40,7 @@ class Mailer < ActionMailer::Base
'Issue-Id' => issue.id,
'Issue-Author' => issue.author.login
redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
@author = journal.user
recipients issue.recipients
# Watchers in cc
cc(issue.watcher_recipients - @recipients)
@@ -73,6 +75,9 @@ class Mailer < ActionMailer::Base
added_to = ''
added_to_url = ''
case container.class.name
when 'Project'
added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container)
added_to = "#{l(:label_project)}: #{container}"
when 'Version'
added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container.project_id)
added_to = "#{l(:label_version)}: #{container.name}"
@@ -205,9 +210,10 @@ class Mailer < ActionMailer::Base
def create_mail
# Removes the current user from the recipients and cc
# if he doesn't want to receive notifications about what he does
if User.current.pref[:no_self_notified]
recipients.delete(User.current.mail) if recipients
cc.delete(User.current.mail) if cc
@author ||= User.current
if @author.pref[:no_self_notified]
recipients.delete(@author.mail) if recipients
cc.delete(@author.mail) if cc
end
# Blind carbon copy recipients
if Setting.bcc_recipients?

View File

@@ -19,11 +19,11 @@ class Message < ActiveRecord::Base
belongs_to :board
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
acts_as_tree :counter_cache => :replies_count, :order => "#{Message.table_name}.created_on ASC"
has_many :attachments, :as => :container, :dependent => :destroy
acts_as_attachable
belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id'
acts_as_searchable :columns => ['subject', 'content'],
:include => {:board, :project},
:include => {:board => :project},
:project_key => 'project_id',
:date_column => "#{table_name}.created_on"
acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},

View File

@@ -44,6 +44,8 @@ class Project < ActiveRecord::Base
:association_foreign_key => 'custom_field_id'
acts_as_tree :order => "name", :counter_cache => true
acts_as_attachable :view_permission => :view_files,
:delete_permission => :manage_files
acts_as_customizable
acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
@@ -58,7 +60,7 @@ class Project < ActiveRecord::Base
validates_associated :repository, :wiki
validates_length_of :name, :maximum => 30
validates_length_of :homepage, :maximum => 255
validates_length_of :identifier, :in => 3..20
validates_length_of :identifier, :in => 2..20
validates_format_of :identifier, :with => /^[a-z0-9\-]*$/
before_destroy :delete_all_members

View File

@@ -31,9 +31,9 @@ class Role < ActiveRecord::Base
raise "Can not copy workflow from a #{role.class}" unless role.is_a?(Role)
raise "Can not copy workflow from/to an unsaved role" if proxy_owner.new_record? || role.new_record?
clear
connection.insert "INSERT INTO workflows (tracker_id, old_status_id, new_status_id, role_id)" +
connection.insert "INSERT INTO #{Workflow.table_name} (tracker_id, old_status_id, new_status_id, role_id)" +
" SELECT tracker_id, old_status_id, new_status_id, #{proxy_owner.id}" +
" FROM workflows" +
" FROM #{Workflow.table_name}" +
" WHERE role_id = #{role.id}"
end
end

View File

@@ -32,7 +32,7 @@ class TimeEntry < ActiveRecord::Base
:description => :comments
validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on
validates_numericality_of :hours, :allow_nil => true
validates_numericality_of :hours, :allow_nil => true, :message => :activerecord_error_invalid
validates_length_of :comments, :maximum => 255, :allow_nil => true
def after_initialize
@@ -54,7 +54,7 @@ class TimeEntry < ActiveRecord::Base
end
def hours=(h)
write_attribute :hours, (h.is_a?(String) ? h.to_hours : h)
write_attribute :hours, (h.is_a?(String) ? (h.to_hours || h) : h)
end
# tyear, tmonth, tweek assigned where setting spent_on attributes

View File

@@ -23,9 +23,9 @@ class Tracker < ActiveRecord::Base
raise "Can not copy workflow from a #{tracker.class}" unless tracker.is_a?(Tracker)
raise "Can not copy workflow from/to an unsaved tracker" if proxy_owner.new_record? || tracker.new_record?
clear
connection.insert "INSERT INTO workflows (tracker_id, old_status_id, new_status_id, role_id)" +
connection.insert "INSERT INTO #{Workflow.table_name} (tracker_id, old_status_id, new_status_id, role_id)" +
" SELECT #{proxy_owner.id}, old_status_id, new_status_id, role_id" +
" FROM workflows" +
" FROM #{Workflow.table_name}" +
" WHERE tracker_id = #{tracker.id}"
end
end

View File

@@ -19,7 +19,8 @@ class Version < ActiveRecord::Base
before_destroy :check_integrity
belongs_to :project
has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id'
has_many :attachments, :as => :container, :dependent => :destroy
acts_as_attachable :view_permission => :view_files,
:delete_permission => :manage_files
validates_presence_of :name
validates_uniqueness_of :name, :scope => [:project_id]

View File

@@ -21,7 +21,7 @@ require 'enumerator'
class WikiPage < ActiveRecord::Base
belongs_to :wiki
has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy
has_many :attachments, :as => :container, :dependent => :destroy
acts_as_attachable :delete_permission => :delete_wiki_pages_attachments
acts_as_tree :order => 'title'
acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"},
@@ -111,6 +111,10 @@ class WikiPage < ActiveRecord::Base
def editable_by?(usr)
!protected? || usr.allowed_to?(:protect_wiki_pages, wiki.project)
end
def attachments_deletable?(usr=User.current)
editable_by?(usr) && super(usr)
end
def parent_title
@parent_title || (self.parent && self.parent.pretty_title)

View File

@@ -4,7 +4,8 @@
<table class="list">
<tr class="odd"><td><%= l(:text_default_administrator_account_changed) %></td><td><%= image_tag (@flags[:default_admin_changed] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %></td></tr>
<tr class="even"><td><%= l(:text_file_repository_writable) %></td><td><%= image_tag (@flags[:file_repository_writable] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %></td></tr>
<tr class="even"><td><%= l(:text_file_repository_writable) %> (<%= Attachment.storage_path %>)</td><td><%= image_tag (@flags[:file_repository_writable] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %></td></tr>
<tr class="even"><td><%= l(:text_plugin_assets_writable) %> (<%= Engines.public_directory %>)</td><td><%= image_tag (@flags[:plugin_assets_writable] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %></td></tr>
<tr class="odd"><td><%= l(:text_rmagick_available) %></td><td><%= image_tag (@flags[:rmagick_available] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %></td></tr>
</table>

View File

@@ -3,14 +3,14 @@
<p><%= link_to_attachment attachment, :class => 'icon icon-attachment' -%>
<%= h(" - #{attachment.description}") unless attachment.description.blank? %>
<span class="size">(<%= number_to_human_size attachment.filesize %>)</span>
<% if options[:delete_url] %>
<%= link_to image_tag('delete.png'), options[:delete_url].update({:attachment_id => attachment}),
<% if options[:deletable] %>
<%= link_to image_tag('delete.png'), {:controller => 'attachments', :action => 'destroy', :id => attachment},
:confirm => l(:text_are_you_sure),
:method => :post,
:class => 'delete',
:title => l(:button_delete) %>
<% end %>
<% unless options[:no_author] %>
<% if options[:author] %>
<span class="author"><%= attachment.author %>, <%= format_time(attachment.created_on) %></span>
<% end %>
</p>

View File

@@ -12,7 +12,7 @@
</div>
<h3><%= l(:label_attachment_plural) %></h3>
<%= link_to_attachments @attachments, :delete_url => (authorize_for('documents', 'destroy_attachment') ? {:controller => 'documents', :action => 'destroy_attachment', :id => @document} : nil) %>
<%= link_to_attachments @document %>
<% if authorize_for('documents', 'add_attachment') %>
<p><%= link_to l(:label_attachment_new), {}, :onclick => "Element.show('add_attachment_form'); Element.hide(this); Element.scrollTo('add_attachment_form'); return false;",

View File

@@ -8,7 +8,7 @@
<div id="issue_descr_fields" <%= 'style="display:none"' unless @issue.new_record? || @issue.errors.any? %>>
<p><%= f.text_field :subject, :size => 80, :required => true %></p>
<p><%= f.text_area :description, :required => true,
<p><%= f.text_area :description,
:cols => 60,
:rows => (@issue.description.blank? ? 10 : [[10, @issue.description.length / 50].max, 100].min),
:accesskey => accesskey(:edit),
@@ -24,11 +24,13 @@
<p><%= f.select :priority_id, (@priorities.collect {|p| [p.name, p.id]}), :required => true %></p>
<p><%= f.select :assigned_to_id, (@issue.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true %></p>
<% unless @project.issue_categories.empty? %>
<p><%= f.select :category_id, (@project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true %>
<%= prompt_to_remote(l(:label_issue_category_new),
l(:label_issue_category_new), 'category[name]',
{:controller => 'projects', :action => 'add_issue_category', :id => @project},
:class => 'small', :tabindex => 199) if authorize_for('projects', 'add_issue_category') %></p>
<% end %>
<%= content_tag('p', f.select(:fixed_version_id,
(@project.versions.sort.collect {|v| [v.name, v.id]}),
{ :include_blank => true })) unless @project.versions.empty? %>
@@ -48,6 +50,14 @@
<p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form' %></p>
<% end %>
<% if @issue.new_record? && User.current.allowed_to?(:add_issue_watchers, @project) -%>
<p><label><%= l(:label_issue_watchers) %></label>
<% @issue.project.users.sort.each do |user| -%>
<label class="floating"><%= check_box_tag 'issue[watcher_user_ids][]', user.id, @issue.watcher_user_ids.include?(user.id) %> <%=h user %></label>
<% end -%>
</p>
<% end %>
<%= call_hook(:view_issues_form_details_bottom, { :issue => @issue, :form => f }) %>
<%= wikitoolbar_for 'issue_description' %>

View File

@@ -44,6 +44,7 @@
<fieldset><legend><%= l(:field_notes) %></legend>
<%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
<%= wikitoolbar_for 'notes' %>
</fieldset>
</div>
<p><%= submit_tag l(:button_submit) %>

View File

@@ -1,188 +0,0 @@
<%
pdf=IfpdfHelper::IFPDF.new(current_language)
pdf.SetTitle("#{l(:label_gantt)} #{@project}")
pdf.AliasNbPages
pdf.footer_date = format_date(Date.today)
pdf.AddPage("L")
pdf.SetFontStyle('B',12)
pdf.SetX(15)
pdf.Cell(70, 20, @project.to_s)
pdf.Ln
pdf.SetFontStyle('B',9)
subject_width = 70
header_heigth = 5
headers_heigth = header_heigth
show_weeks = false
show_days = false
if @gantt.months < 7
show_weeks = true
headers_heigth = 2*header_heigth
if @gantt.months < 3
show_days = true
headers_heigth = 3*header_heigth
end
end
g_width = 210
zoom = (g_width) / (@gantt.date_to - @gantt.date_from + 1)
g_height = 120
t_height = g_height + headers_heigth
y_start = pdf.GetY
#
# Months headers
#
month_f = @gantt.date_from
left = subject_width
height = header_heigth
@gantt.months.times do
width = ((month_f >> 1) - month_f) * zoom
pdf.SetY(y_start)
pdf.SetX(left)
pdf.Cell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C")
left = left + width
month_f = month_f >> 1
end
#
# Weeks headers
#
if show_weeks
left = subject_width
height = header_heigth
if @gantt.date_from.cwday == 1
# @gantt.date_from is monday
week_f = @gantt.date_from
else
# find next monday after @gantt.date_from
week_f = @gantt.date_from + (7 - @gantt.date_from.cwday + 1)
width = (7 - @gantt.date_from.cwday + 1) * zoom-1
pdf.SetY(y_start + header_heigth)
pdf.SetX(left)
pdf.Cell(width + 1, height, "", "LTR")
left = left + width+1
end
while week_f <= @gantt.date_to
width = (week_f + 6 <= @gantt.date_to) ? 7 * zoom : (@gantt.date_to - week_f + 1) * zoom
pdf.SetY(y_start + header_heigth)
pdf.SetX(left)
pdf.Cell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C")
left = left + width
week_f = week_f+7
end
end
#
# Days headers
#
if show_days
left = subject_width
height = header_heigth
wday = @gantt.date_from.cwday
pdf.SetFontStyle('B',7)
(@gantt.date_to - @gantt.date_from + 1).to_i.times do
width = zoom
pdf.SetY(y_start + 2 * header_heigth)
pdf.SetX(left)
pdf.Cell(width, height, day_name(wday).first, "LTR", 0, "C")
left = left + width
wday = wday + 1
wday = 1 if wday > 7
end
end
pdf.SetY(y_start)
pdf.SetX(15)
pdf.Cell(subject_width+g_width-15, headers_heigth, "", 1)
#
# Tasks
#
top = headers_heigth + y_start
pdf.SetFontStyle('B',7)
@gantt.events.each do |i|
pdf.SetY(top)
pdf.SetX(15)
if i.is_a? Issue
pdf.Cell(subject_width-15, 5, "#{i.tracker.name} #{i.id}: #{i.subject}".sub(/^(.{30}[^\s]*\s).*$/, '\1 (...)'), "LR")
else
pdf.Cell(subject_width-15, 5, "#{l(:label_version)}: #{i.name}", "LR")
end
pdf.SetY(top)
pdf.SetX(subject_width)
pdf.Cell(g_width, 5, "", "LR")
pdf.SetY(top+1.5)
if i.is_a? Issue
i_start_date = (i.start_date >= @gantt.date_from ? i.start_date : @gantt.date_from )
i_end_date = (i.due_before <= @gantt.date_to ? i.due_before : @gantt.date_to )
i_done_date = i.start_date + ((i.due_before - i.start_date+1)*i.done_ratio/100).floor
i_done_date = (i_done_date <= @gantt.date_from ? @gantt.date_from : i_done_date )
i_done_date = (i_done_date >= @gantt.date_to ? @gantt.date_to : i_done_date )
i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
i_left = ((i_start_date - @gantt.date_from)*zoom)
i_width = ((i_end_date - i_start_date + 1)*zoom)
d_width = ((i_done_date - i_start_date)*zoom)
l_width = ((i_late_date - i_start_date+1)*zoom) if i_late_date
l_width ||= 0
pdf.SetX(subject_width + i_left)
pdf.SetFillColor(200,200,200)
pdf.Cell(i_width, 2, "", 0, 0, "", 1)
if l_width > 0
pdf.SetY(top+1.5)
pdf.SetX(subject_width + i_left)
pdf.SetFillColor(255,100,100)
pdf.Cell(l_width, 2, "", 0, 0, "", 1)
end
if d_width > 0
pdf.SetY(top+1.5)
pdf.SetX(subject_width + i_left)
pdf.SetFillColor(100,100,255)
pdf.Cell(d_width, 2, "", 0, 0, "", 1)
end
pdf.SetY(top+1.5)
pdf.SetX(subject_width + i_left + i_width)
pdf.Cell(30, 2, "#{i.status.name} #{i.done_ratio}%")
else
i_left = ((i.start_date - @gantt.date_from)*zoom)
pdf.SetX(subject_width + i_left)
pdf.SetFillColor(50,200,50)
pdf.Cell(2, 2, "", 0, 0, "", 1)
pdf.SetY(top+1.5)
pdf.SetX(subject_width + i_left + 3)
pdf.Cell(30, 2, "#{i.name}")
end
top = top + 5
pdf.SetDrawColor(200, 200, 200)
pdf.Line(15, top, subject_width+g_width, top)
if pdf.GetY() > 180
pdf.AddPage("L")
top = 20
pdf.Line(15, top, subject_width+g_width, top)
end
pdf.SetDrawColor(0, 0, 0)
end
pdf.Line(15, top, subject_width+g_width, top)
%>
<%= pdf.Output %>

View File

@@ -1,50 +0,0 @@
<% pdf=IfpdfHelper::IFPDF.new(current_language)
title = @project ? "#{@project.name} - #{l(:label_issue_plural)}" : "#{l(:label_issue_plural)}"
pdf.SetTitle(title)
pdf.AliasNbPages
pdf.footer_date = format_date(Date.today)
pdf.AddPage("L")
row_height = 7
#
# title
#
pdf.SetFontStyle('B',11)
pdf.Cell(190,10, title)
pdf.Ln
#
# headers
#
pdf.SetFontStyle('B',10)
pdf.SetFillColor(230, 230, 230)
pdf.Cell(15, row_height, "#", 0, 0, 'L', 1)
pdf.Cell(30, row_height, l(:field_tracker), 0, 0, 'L', 1)
pdf.Cell(30, row_height, l(:field_status), 0, 0, 'L', 1)
pdf.Cell(30, row_height, l(:field_priority), 0, 0, 'L', 1)
pdf.Cell(40, row_height, l(:field_assigned_to), 0, 0, 'L', 1)
pdf.Cell(25, row_height, l(:field_updated_on), 0, 0, 'L', 1)
pdf.Cell(0, row_height, l(:field_subject), 0, 0, 'L', 1)
pdf.Line(10, pdf.GetY, 287, pdf.GetY)
pdf.Ln
pdf.Line(10, pdf.GetY, 287, pdf.GetY)
pdf.SetY(pdf.GetY() + 1)
#
# rows
#
pdf.SetFontStyle('',9)
pdf.SetFillColor(255, 255, 255)
@issues.each do |issue|
pdf.Cell(15, row_height, issue.id.to_s, 0, 0, 'L', 1)
pdf.Cell(30, row_height, issue.tracker.name, 0, 0, 'L', 1)
pdf.Cell(30, row_height, issue.status.name, 0, 0, 'L', 1)
pdf.Cell(30, row_height, issue.priority.name, 0, 0, 'L', 1)
pdf.Cell(40, row_height, issue.assigned_to ? issue.assigned_to.name : '', 0, 0, 'L', 1)
pdf.Cell(25, row_height, format_date(issue.updated_on), 0, 0, 'L', 1)
pdf.MultiCell(0, row_height, (@project == issue.project ? issue.subject : "#{issue.project.name} - #{issue.subject}"))
pdf.Line(10, pdf.GetY, 287, pdf.GetY)
pdf.SetY(pdf.GetY() + 1)
end
%>
<%= pdf.Output %>

View File

@@ -7,6 +7,7 @@
<%= render :partial => 'issues/form', :locals => {:f => f} %>
</div>
<%= submit_tag l(:button_create) %>
<%= submit_tag l(:button_create_and_continue), :name => 'continue' %>
<%= link_to_remote l(:label_preview),
{ :url => { :controller => 'issues', :action => 'preview', :project_id => @project },
:method => 'post',
@@ -14,6 +15,12 @@
:with => "Form.serialize('issue-form')",
:complete => "Element.scrollTo('preview')"
}, :accesskey => accesskey(:preview) %>
<%= javascript_tag "Form.Element.focus('issue_subject');" %>
<% end %>
<div id="preview" class="wiki"></div>
<% content_for :header_tags do %>
<%= stylesheet_link_tag 'scm' %>
<% end %>

View File

@@ -1,126 +0,0 @@
<% pdf=IfpdfHelper::IFPDF.new(current_language)
pdf.SetTitle("#{@project.name} - ##{@issue.tracker.name} #{@issue.id}")
pdf.AliasNbPages
pdf.footer_date = format_date(Date.today)
pdf.AddPage
pdf.SetFontStyle('B',11)
pdf.Cell(190,10, "#{@issue.project} - #{@issue.tracker} # #{@issue.id}: #{@issue.subject}")
pdf.Ln
y0 = pdf.GetY
pdf.SetFontStyle('B',9)
pdf.Cell(35,5, l(:field_status) + ":","LT")
pdf.SetFontStyle('',9)
pdf.Cell(60,5, @issue.status.name,"RT")
pdf.SetFontStyle('B',9)
pdf.Cell(35,5, l(:field_priority) + ":","LT")
pdf.SetFontStyle('',9)
pdf.Cell(60,5, @issue.priority.name,"RT")
pdf.Ln
pdf.SetFontStyle('B',9)
pdf.Cell(35,5, l(:field_author) + ":","L")
pdf.SetFontStyle('',9)
pdf.Cell(60,5, @issue.author.name,"R")
pdf.SetFontStyle('B',9)
pdf.Cell(35,5, l(:field_category) + ":","L")
pdf.SetFontStyle('',9)
pdf.Cell(60,5, (@issue.category ? @issue.category.name : "-"),"R")
pdf.Ln
pdf.SetFontStyle('B',9)
pdf.Cell(35,5, l(:field_created_on) + ":","L")
pdf.SetFontStyle('',9)
pdf.Cell(60,5, format_date(@issue.created_on),"R")
pdf.SetFontStyle('B',9)
pdf.Cell(35,5, l(:field_assigned_to) + ":","L")
pdf.SetFontStyle('',9)
pdf.Cell(60,5, (@issue.assigned_to ? @issue.assigned_to.name : "-"),"R")
pdf.Ln
pdf.SetFontStyle('B',9)
pdf.Cell(35,5, l(:field_updated_on) + ":","LB")
pdf.SetFontStyle('',9)
pdf.Cell(60,5, format_date(@issue.updated_on),"RB")
pdf.SetFontStyle('B',9)
pdf.Cell(35,5, l(:field_due_date) + ":","LB")
pdf.SetFontStyle('',9)
pdf.Cell(60,5, format_date(@issue.due_date),"RB")
pdf.Ln
for custom_value in @issue.custom_values
pdf.SetFontStyle('B',9)
pdf.Cell(35,5, custom_value.custom_field.name + ":","L")
pdf.SetFontStyle('',9)
pdf.MultiCell(155,5, (show_value custom_value),"R")
end
pdf.SetFontStyle('B',9)
pdf.Cell(35,5, l(:field_subject) + ":","LTB")
pdf.SetFontStyle('',9)
pdf.Cell(155,5, @issue.subject,"RTB")
pdf.Ln
pdf.SetFontStyle('B',9)
pdf.Cell(35,5, l(:field_description) + ":")
pdf.SetFontStyle('',9)
pdf.MultiCell(155,5, @issue.description,"BR")
pdf.Line(pdf.GetX, y0, pdf.GetX, pdf.GetY)
pdf.Line(pdf.GetX, pdf.GetY, 170, pdf.GetY)
pdf.Ln
if @issue.changesets.any? && User.current.allowed_to?(:view_changesets, @issue.project)
pdf.SetFontStyle('B',9)
pdf.Cell(190,5, l(:label_associated_revisions), "B")
pdf.Ln
for changeset in @issue.changesets
pdf.SetFontStyle('B',8)
pdf.Cell(190,5, format_time(changeset.committed_on) + " - " + changeset.author.to_s)
pdf.Ln
unless changeset.comments.blank?
pdf.SetFontStyle('',8)
pdf.MultiCell(190,5, changeset.comments)
end
pdf.Ln
end
end
pdf.SetFontStyle('B',9)
pdf.Cell(190,5, l(:label_history), "B")
pdf.Ln
for journal in @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
pdf.SetFontStyle('B',8)
pdf.Cell(190,5, format_time(journal.created_on) + " - " + journal.user.name)
pdf.Ln
pdf.SetFontStyle('I',8)
for detail in journal.details
pdf.Cell(190,5, "- " + show_detail(detail, true))
pdf.Ln
end
if journal.notes?
pdf.SetFontStyle('',8)
pdf.MultiCell(190,5, journal.notes)
end
pdf.Ln
end
if @issue.attachments.any?
pdf.SetFontStyle('B',9)
pdf.Cell(190,5, l(:label_attachment_plural), "B")
pdf.Ln
for attachment in @issue.attachments
pdf.SetFontStyle('',8)
pdf.Cell(80,5, attachment.filename)
pdf.Cell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
pdf.Cell(25,5, format_date(attachment.created_on),0,0,"R")
pdf.Cell(65,5, attachment.author.name,0,0,"R")
pdf.Ln
end
end
%>
<%= pdf.Output %>

View File

@@ -19,11 +19,11 @@
<table width="100%">
<tr>
<td style="width:15%" class="status"><b><%=l(:field_status)%>:</b></td><td style="width:35%" class="status status-<%= @issue.status.name %>"><%= @issue.status.name %></td>
<td style="width:15%" class="status"><b><%=l(:field_status)%>:</b></td><td style="width:35%" class="status"><%= @issue.status.name %></td>
<td style="width:15%" class="start-date"><b><%=l(:field_start_date)%>:</b></td><td style="width:35%"><%= format_date(@issue.start_date) %></td>
</tr>
<tr>
<td class="priority"><b><%=l(:field_priority)%>:</b></td><td class="priority priority-<%= @issue.priority.name %>"><%= @issue.priority.name %></td>
<td class="priority"><b><%=l(:field_priority)%>:</b></td><td class="priority"><%= @issue.priority.name %></td>
<td class="due-date"><b><%=l(:field_due_date)%>:</b></td><td class="due-date"><%= format_date(@issue.due_date) %></td>
</tr>
<tr>
@@ -59,7 +59,7 @@ end %>
<hr />
<div class="contextual">
<%= link_to_remote_if_authorized l(:button_quote), { :url => {:action => 'reply', :id => @issue} }, :class => 'icon icon-comment' %>
<%= link_to_remote_if_authorized(l(:button_quote), { :url => {:action => 'reply', :id => @issue} }, :class => 'icon icon-comment') unless @issue.description.blank? %>
</div>
<p><strong><%=l(:field_description)%></strong></p>
@@ -67,9 +67,7 @@ end %>
<%= textilizable @issue, :description, :attachments => @issue.attachments %>
</div>
<% if @issue.attachments.any? %>
<%= link_to_attachments @issue.attachments, :delete_url => (authorize_for('issues', 'destroy_attachment') ? {:controller => 'issues', :action => 'destroy_attachment', :id => @issue} : nil) %>
<% end %>
<%= link_to_attachments @issue %>
<% if authorize_for('issue_relations', 'new') || @issue.relations.any? %>
<hr />

View File

@@ -3,10 +3,10 @@
<option selected="selected"><%= l(:label_jump_to_a_project) %></option>
<option disabled="disabled">---</option>
<% user_projects_by_root.keys.sort.each do |root| %>
<%= content_tag('option', h(root.name), :value => url_for(:controller => 'projects', :action => 'show', :id => root)) %>
<%= content_tag('option', h(root.name), :value => url_for(:controller => 'projects', :action => 'show', :id => root, :jump => current_menu_item)) %>
<% user_projects_by_root[root].sort.each do |project| %>
<% next if project == root %>
<%= content_tag('option', ('&#187; ' + h(project.name)), :value => url_for(:controller => 'projects', :action => 'show', :id => project)) %>
<%= content_tag('option', ('&#187; ' + h(project.name)), :value => url_for(:controller => 'projects', :action => 'show', :id => project, :jump => current_menu_item)) %>
<% end %>
<% end %>
</select>

View File

@@ -50,8 +50,7 @@
</div>
<div id="content">
<%= content_tag('div', flash[:error], :class => 'flash error') if flash[:error] %>
<%= content_tag('div', flash[:notice], :class => 'flash notice') if flash[:notice] %>
<%= render_flash_messages %>
<%= yield %>
</div>
</div>

View File

@@ -6,17 +6,12 @@ body {
font-size: 0.8em;
color:#484848;
}
h1 {
font-family: "Trebuchet MS", Verdana, sans-serif;
font-size: 1.2em;
margin: 0px;
}
a, a:link, a:visited {
color: #2A5685;
}
a:hover, a:active {
color: #c61a1a;
}
h1, h2, h3 { font-family: "Trebuchet MS", Verdana, sans-serif; margin: 0px; }
h1 { font-size: 1.2em; }
h2, h3 { font-size: 1.1em; }
a, a:link, a:visited { color: #2A5685;}
a:hover, a:active { color: #c61a1a; }
a.wiki-anchor { display: none; }
hr {
width: 100%;
height: 1px;

View File

@@ -15,7 +15,7 @@
<div class="wiki">
<%= textilizable(@topic.content, :attachments => @topic.attachments) %>
</div>
<%= link_to_attachments @topic.attachments, :no_author => true %>
<%= link_to_attachments @topic, :author => false %>
</div>
<br />
@@ -31,7 +31,7 @@
<div class="message reply">
<h4><%=h message.subject %> - <%= authoring message.created_on, message.author %></h4>
<div class="wiki"><%= textilizable message, :content, :attachments => message.attachments %></div>
<%= link_to_attachments message.attachments, :no_author => true %>
<%= link_to_attachments message, :author => false %>
</div>
<% end %>
<% end %>

View File

@@ -47,6 +47,6 @@ entries_by_day = entries.group_by(&:spent_on)
</tr>
<% end -%>
<% end -%>
</tbdoy>
</tbody>
</table>
<% end %>

View File

@@ -11,7 +11,7 @@
<p><%= f.text_area :description, :rows => 5, :class => 'wiki-edit' %></p>
<p><%= f.text_field :identifier, :required => true, :disabled => @project.identifier_frozen? %>
<% unless @project.identifier_frozen? %>
<br /><em><%= l(:text_length_between, 3, 20) %> <%= l(:text_project_identifier_info) %></em>
<br /><em><%= l(:text_length_between, 2, 20) %> <%= l(:text_project_identifier_info) %></em>
<% end %></p>
<p><%= f.text_field :homepage, :size => 60 %></p>
<p><%= f.check_box :is_public %></p>

View File

@@ -4,10 +4,13 @@
<div class="box">
<% form_tag({ :action => 'add_file', :id => @project }, :multipart => true, :class => "tabular") do %>
<p><label for="version_id"><%=l(:field_version)%> <span class="required">*</span></label>
<%= select_tag "version_id", options_from_collection_for_select(@versions, "id", "name") %></p>
<% if @versions.any? %>
<p><label for="version_id"><%=l(:field_version)%></label>
<%= select_tag "version_id", content_tag('option', '') +
options_from_collection_for_select(@versions, "id", "name") %></p>
<% end %>
<p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form' %></p>
</div>
<%= submit_tag l(:button_add) %>
<% end %>
<% end %>

View File

@@ -4,39 +4,37 @@
<h2><%=l(:label_attachment_plural)%></h2>
<% delete_allowed = authorize_for('versions', 'destroy_file') %>
<% delete_allowed = User.current.allowed_to?(:manage_files, @project) %>
<table class="list">
<thead><tr>
<th><%=l(:field_version)%></th>
<%= sort_header_tag('filename', :caption => l(:field_filename)) %>
<%= sort_header_tag('created_on', :caption => l(:label_date), :default_order => 'desc') %>
<%= sort_header_tag('size', :caption => l(:field_filesize), :default_order => 'desc') %>
<%= sort_header_tag('downloads', :caption => l(:label_downloads_abbr), :default_order => 'desc') %>
<th>MD5</th>
<% if delete_allowed %><th></th><% end %>
<th></th>
</tr></thead>
<tbody>
<% for version in @versions %>
<% unless version.attachments.empty? %>
<tr><th colspan="7" align="left"><span class="icon icon-package"><b><%= version.name %></b></span></th></tr>
<% for file in version.attachments %>
<% @containers.each do |container| %>
<% next if container.attachments.empty? -%>
<% if container.is_a?(Version) -%>
<tr><th colspan="6" align="left"><span class="icon icon-package"><b><%=h container %></b></span></th></tr>
<% end -%>
<% container.attachments.each do |file| %>
<tr class="<%= cycle("odd", "even") %>">
<td></td>
<td><%= link_to_attachment file, :download => true, :title => file.description %></td>
<td align="center"><%= format_time(file.created_on) %></td>
<td align="center"><%= number_to_human_size(file.filesize) %></td>
<td align="center"><%= file.downloads %></td>
<td align="center"><small><%= file.digest %></small></td>
<% if delete_allowed %>
<td align="center">
<%= link_to_if_authorized image_tag('delete.png'), {:controller => 'versions', :action => 'destroy_file', :id => version, :attachment_id => file}, :confirm => l(:text_are_you_sure), :method => :post %>
<%= link_to(image_tag('delete.png'), {:controller => 'attachments', :action => 'destroy', :id => file},
:confirm => l(:text_are_you_sure), :method => :post) if delete_allowed %>
</td>
<% end %>
</tr>
<% end
reset_cycle %>
<% end %>
<% end %>
</tbody>
</table>

View File

@@ -17,7 +17,6 @@
<td><%= link_to(version.wiki_page_title, :controller => 'wiki', :page => Wiki.titleize(version.wiki_page_title)) unless version.wiki_page_title.blank? || @project.wiki.nil? %></td>
<td align="center"><%= link_to_if_authorized l(:button_edit), { :controller => 'versions', :action => 'edit', :id => version }, :class => 'icon icon-edit' %></td>
<td align="center"><%= link_to_if_authorized l(:button_delete), {:controller => 'versions', :action => 'destroy', :id => version}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %></td>
</td>
</tr>
<% end; reset_cycle %>
</tbody>

View File

@@ -0,0 +1,10 @@
<p>
<% if @repository.supports_cat? %>
<%= link_to_if action_name != 'entry', l(:button_view), {:action => 'entry', :id => @project, :path => to_path_param(@path), :rev => @rev } %> |
<% end %>
<% if @repository.supports_annotate? %>
<%= link_to_if action_name != 'annotate', l(:button_annotate), {:action => 'annotate', :id => @project, :path => to_path_param(@path), :rev => @rev } %> |
<% end %>
<%= link_to(l(:button_download), {:action => 'entry', :id => @project, :path => to_path_param(@path), :rev => @rev, :format => 'raw' }) if @repository.supports_cat? %>
<%= "(#{number_to_human_size(@entry.size)})" if @entry.size %>
</p>

View File

@@ -1,5 +1,7 @@
<h2><%= render :partial => 'navigation', :locals => { :path => @path, :kind => 'file', :revision => @rev } %></h2>
<p><%= render :partial => 'link_to_functions' %></p>
<% colors = Hash.new {|k,v| k[v] = (k.size % 12) } %>
<div class="autoscroll">

View File

@@ -1,15 +1,6 @@
<h2><%= render :partial => 'navigation', :locals => { :path => @path, :kind => (@entry ? @entry.kind : nil), :revision => @rev } %></h2>
<p>
<% if @repository.supports_cat? %>
<%= link_to l(:button_view), {:action => 'entry', :id => @project, :path => to_path_param(@path), :rev => @rev } %> |
<% end %>
<% if @repository.supports_annotate? %>
<%= link_to l(:button_annotate), {:action => 'annotate', :id => @project, :path => to_path_param(@path), :rev => @rev } %> |
<% end %>
<%= link_to(l(:button_download), {:action => 'entry', :id => @project, :path => to_path_param(@path), :rev => @rev, :format => 'raw' }) if @repository.supports_cat? %>
<%= "(#{number_to_human_size(@entry.size)})" if @entry.size %>
</p>
<p><%= render :partial => 'link_to_functions' %></p>
<%= render_properties(@properties) %>

View File

@@ -1,5 +1,7 @@
<h2><%= render :partial => 'navigation', :locals => { :path => @path, :kind => 'file', :revision => @rev } %></h2>
<p><%= render :partial => 'link_to_functions' %></p>
<%= render :partial => 'common/file', :locals => {:filename => @path, :content => @content} %>
<% content_for :header_tags do %>

View File

@@ -5,7 +5,7 @@
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<style>
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; }
ul.toc { padding: 4px; margin-left: 0; }
ul.toc li { list-style-type:none; }
ul.toc li.heading2 { margin-left: 1em; }

View File

@@ -5,7 +5,7 @@
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<style>
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; }
</style>
</head>
<body>

View File

@@ -28,7 +28,7 @@
<%= render(:partial => "wiki/content", :locals => {:content => @content}) %>
<%= link_to_attachments @page.attachments, :delete_url => ((@editable && authorize_for('wiki', 'destroy_attachment')) ? {:controller => 'wiki', :action => 'destroy_attachment', :page => @page.title} : nil) %>
<%= link_to_attachments @page %>
<% if @editable && authorize_for('wiki', 'add_attachment') %>
<p><%= link_to l(:label_attachment_new), {}, :onclick => "Element.show('add_attachment_form'); Element.hide(this); Element.scrollTo('add_attachment_form'); return false;",

View File

@@ -2,9 +2,10 @@ class SetTopicAuthorsAsWatchers < ActiveRecord::Migration
def self.up
# Sets active users who created/replied a topic as watchers of the topic
# so that the new watch functionality at topic level doesn't affect notifications behaviour
Message.connection.execute("INSERT INTO watchers (watchable_type, watchable_id, user_id)" +
" SELECT DISTINCT 'Message', COALESCE(messages.parent_id, messages.id), messages.author_id FROM messages, users" +
" WHERE messages.author_id = users.id AND users.status = 1")
Message.connection.execute("INSERT INTO #{Watcher.table_name} (watchable_type, watchable_id, user_id)" +
" SELECT DISTINCT 'Message', COALESCE(m.parent_id, m.id), m.author_id" +
" FROM #{Message.table_name} m, #{User.table_name} u" +
" WHERE m.author_id = u.id AND u.status = 1")
end
def self.down

View File

@@ -1,10 +1,37 @@
== Redmine changelog
Redmine - project management software
Copyright (C) 2006-2008 Jean-Philippe Lang
Copyright (C) 2006-2009 Jean-Philippe Lang
http://www.redmine.org/
== 2009-02-15 v0.8.1
* Select watchers on new issue form
* Issue description is no longer a required field
* Files module: ability to add files without version
* Jump to the current tab when using the project quick-jump combo
* Display a warning if some attachments were not saved
* Import custom fields values from emails on issue creation
* Show view/annotate/download links on entry and annotate views
* Admin Info Screen: Display if plugin assets directory is writable
* Adds a 'Create and continue' button on the new issue form
* IMAP: add options to move received emails
* Do not show Category field when categories are not defined
* Lower the project identifier limit to a minimum of two characters
* Add "closed" html class to closed entries in issue list
* Fixed: broken redirect URL on login failure
* Fixed: Deleted files are shown when using Darcs
* Fixed: Darcs adapter works on Win32 only
* Fixed: syntax highlight doesn't appear in new ticket preview
* Fixed: email notification for changes I make still occurs when running Repository.fetch_changesets
* Fixed: no error is raised when entering invalid hours on the issue update form
* Fixed: Details time log report CSV export doesn't honour date format from settings
* Fixed: invalid css classes on issue details
* Fixed: Trac importer creates duplicate custom values
* Fixed: inline attached image should not match partial filename
== 2008-12-30 v0.8.0
* Setting added in order to limit the number of diff lines that should be displayed

View File

@@ -696,3 +696,6 @@ label_user_activity: "%s's activity"
label_updated_time_by: Updated by %s %s ago
text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
setting_diff_max_lines_displayed: Max number of diff lines displayed
text_plugin_assets_writable: Plugin assets directory writable
warning_attachments_not_saved: "%d file(s) could not be saved."
button_create_and_continue: Create and continue

View File

@@ -697,3 +697,6 @@ label_user_activity: "%s's activity"
label_updated_time_by: Updated by %s %s ago
text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
setting_diff_max_lines_displayed: Max number of diff lines displayed
text_plugin_assets_writable: Plugin assets directory writable
warning_attachments_not_saved: "%d file(s) could not be saved."
button_create_and_continue: Create and continue

View File

@@ -701,3 +701,6 @@ label_user_activity: "%s's activity"
label_updated_time_by: Updated by %s %s ago
text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
setting_diff_max_lines_displayed: Max number of diff lines displayed
text_plugin_assets_writable: Plugin assets directory writable
warning_attachments_not_saved: "%d file(s) could not be saved."
button_create_and_continue: Create and continue

View File

@@ -697,3 +697,6 @@ label_user_activity: "%s's activity"
label_updated_time_by: Updated by %s %s ago
text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
setting_diff_max_lines_displayed: Max number of diff lines displayed
text_plugin_assets_writable: Plugin assets directory writable
warning_attachments_not_saved: "%d file(s) could not be saved."
button_create_and_continue: Create and continue

View File

@@ -698,3 +698,6 @@ default_activity_development: Entwicklung
enumeration_issue_priorities: Ticket-Prioritäten
enumeration_doc_categories: Dokumentenkategorien
enumeration_activities: Aktivitäten (Zeiterfassung)
text_plugin_assets_writable: Plugin assets directory writable
warning_attachments_not_saved: "%d file(s) could not be saved."
button_create_and_continue: Create and continue

View File

@@ -85,6 +85,8 @@ error_scm_command_failed: "An error occurred when trying to access the repositor
error_scm_annotate: "The entry does not exist or can not be annotated."
error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
warning_attachments_not_saved: "%d file(s) could not be saved."
mail_subject_lost_password: Your %s password
mail_body_lost_password: 'To change your password, click on the following link:'
mail_subject_register: Your %s account activation
@@ -590,6 +592,7 @@ button_check_all: Check all
button_uncheck_all: Uncheck all
button_delete: Delete
button_create: Create
button_create_and_continue: Create and continue
button_test: Test
button_edit: Edit
button_add: Add
@@ -660,7 +663,8 @@ text_status_changed_by_changeset: Applied in changeset %s.
text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
text_select_project_modules: 'Select modules to enable for this project:'
text_default_administrator_account_changed: Default administrator account changed
text_file_repository_writable: File repository writable
text_file_repository_writable: Attachments directory writable
text_plugin_assets_writable: Plugin assets directory writable
text_rmagick_available: RMagick available (optional)
text_destroy_time_entries_question: %.02f hours were reported on the issues you are about to delete. What do you want to do ?
text_destroy_time_entries: Delete reported hours

View File

@@ -681,3 +681,6 @@ text_user_mail_option: "De los proyectos no seleccionados, sólo recibirá notif
text_user_wrote: '%s escribió:'
text_wiki_destroy_confirmation: ¿Seguro que quiere borrar el wiki y todo su contenido?
text_workflow_edit: Seleccionar un flujo de trabajo para actualizar
text_plugin_assets_writable: Plugin assets directory writable
warning_attachments_not_saved: "%d file(s) could not be saved."
button_create_and_continue: Create and continue

View File

@@ -696,3 +696,6 @@ label_user_activity: "Käyttäjän %s historia"
label_updated_time_by: Updated by %s %s ago
text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
setting_diff_max_lines_displayed: Max number of diff lines displayed
text_plugin_assets_writable: Plugin assets directory writable
warning_attachments_not_saved: "%d file(s) could not be saved."
button_create_and_continue: Create and continue

View File

@@ -85,6 +85,8 @@ error_scm_command_failed: "Une erreur s'est produite lors de l'accès au dépôt
error_scm_annotate: "L'entrée n'existe pas ou ne peut pas être annotée."
error_issue_not_found_in_project: "La demande n'existe pas ou n'appartient pas à ce projet"
warning_attachments_not_saved: "%d fichier(s) n'ont pas pu être sauvegardés."
mail_subject_lost_password: Votre mot de passe %s
mail_body_lost_password: 'Pour changer votre mot de passe, cliquez sur le lien suivant:'
mail_subject_register: Activation de votre compte %s
@@ -580,7 +582,7 @@ label_reverse_chronological_order: Dans l'ordre chronologique inverse
label_planning: Planning
label_incoming_emails: Emails entrants
label_generate_key: Générer une clé
label_issue_watchers: Utilisateurs surveillant cette demande
label_issue_watchers: Observateurs
label_example: Exemple
button_login: Connexion
@@ -590,6 +592,7 @@ button_check_all: Tout cocher
button_uncheck_all: Tout décocher
button_delete: Supprimer
button_create: Créer
button_create_and_continue: Créer et continuer
button_test: Tester
button_edit: Modifier
button_add: Ajouter
@@ -661,6 +664,7 @@ text_issues_destroy_confirmation: 'Etes-vous sûr de vouloir supprimer le(s) dem
text_select_project_modules: 'Selectionner les modules à activer pour ce project:'
text_default_administrator_account_changed: Compte administrateur par défaut changé
text_file_repository_writable: Répertoire de stockage des fichiers accessible en écriture
text_plugin_assets_writable: Répertoire public des plugins accessible en écriture
text_rmagick_available: Bibliothèque RMagick présente (optionnelle)
text_destroy_time_entries_question: %.02f heures ont été enregistrées sur les demandes à supprimer. Que voulez-vous faire ?
text_destroy_time_entries: Supprimer les heures

View File

@@ -694,3 +694,8 @@ permission_edit_own_messages: ערוך הודעות של עצמך
permission_delete_own_messages: מחק הודעות של עצמך
label_user_activity: "הפעילות של %s"
label_updated_time_by: עודכן ע"י %s לפני %s
setting_diff_max_lines_displayed: Max number of diff lines displayed
text_plugin_assets_writable: Plugin assets directory writable
text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
warning_attachments_not_saved: "%d file(s) could not be saved."
button_create_and_continue: Create and continue

View File

@@ -697,3 +697,6 @@ label_user_activity: "%s tevékenységei"
label_updated_time_by: "Módosította %s ennyivel ezelőtt: %s"
text_diff_truncated: '... A diff fájl vége nem jelenik meg, mert hosszab, mint a megjeleníthető sorok száma.'
setting_diff_max_lines_displayed: A megjelenítendő sorok száma (maximum) a diff fájloknál
text_plugin_assets_writable: Plugin assets directory writable
warning_attachments_not_saved: "%d file(s) could not be saved."
button_create_and_continue: Create and continue

View File

@@ -696,3 +696,6 @@ label_user_activity: "%s's activity"
label_updated_time_by: Updated by %s %s ago
text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
setting_diff_max_lines_displayed: Max number of diff lines displayed
text_plugin_assets_writable: Plugin assets directory writable
warning_attachments_not_saved: "%d file(s) could not be saved."
button_create_and_continue: Create and continue

View File

@@ -697,3 +697,6 @@ label_user_activity: "%s's activity"
label_updated_time_by: Updated by %s %s ago
text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
setting_diff_max_lines_displayed: Max number of diff lines displayed
text_plugin_assets_writable: Plugin assets directory writable
warning_attachments_not_saved: "%d file(s) could not be saved."
button_create_and_continue: Create and continue

View File

@@ -696,3 +696,6 @@ label_user_activity: "%s의 작업내역"
label_updated_time_by: %s가 %s 전에 변경
text_diff_truncated: '... 이 차이점은 표시할 수 있는 최대 줄수를 초과해서 이 차이점은 잘렸습니다.'
setting_diff_max_lines_displayed: 차이점보기에 표시할 최대 줄수
text_plugin_assets_writable: Plugin assets directory writable
warning_attachments_not_saved: "%d file(s) could not be saved."
button_create_and_continue: Create and continue

View File

@@ -698,3 +698,6 @@ default_activity_development: Vystymas
enumeration_issue_priorities: Darbo prioritetai
enumeration_doc_categories: Dokumento kategorijos
enumeration_activities: Veiklos (laiko sekimas)
text_plugin_assets_writable: Plugin assets directory writable
warning_attachments_not_saved: "%d file(s) could not be saved."
button_create_and_continue: Create and continue

View File

@@ -698,3 +698,6 @@ label_user_activity: "%s's activity"
label_updated_time_by: Updated by %s %s ago
text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
setting_diff_max_lines_displayed: Max number of diff lines displayed
text_plugin_assets_writable: Plugin assets directory writable
warning_attachments_not_saved: "%d file(s) could not be saved."
button_create_and_continue: Create and continue

View File

@@ -697,3 +697,6 @@ label_user_activity: "%s's activity"
label_updated_time_by: Updated by %s %s ago
text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
setting_diff_max_lines_displayed: Max number of diff lines displayed
text_plugin_assets_writable: Plugin assets directory writable
warning_attachments_not_saved: "%d file(s) could not be saved."
button_create_and_continue: Create and continue

View File

@@ -715,3 +715,6 @@ label_user_activity: "Aktywność: %s"
label_updated_time_by: Uaktualnione przez %s %s temu
text_diff_truncated: '... Ten plik różnic został przycięty ponieważ jest zbyt długi.'
setting_diff_max_lines_displayed: Maksymalna liczba linii różnicy do pokazania
text_plugin_assets_writable: Plugin assets directory writable
warning_attachments_not_saved: "%d file(s) could not be saved."
button_create_and_continue: Create and continue

View File

@@ -47,8 +47,8 @@ general_text_Yes: 'Sim'
general_text_no: 'não'
general_text_yes: 'sim'
general_lang_name: 'Português(Brasil)'
general_csv_separator: ','
general_csv_decimal_separator: '.'
general_csv_separator: ';'
general_csv_decimal_separator: ','
general_csv_encoding: ISO-8859-1
general_pdf_encoding: ISO-8859-1
general_day_names: Segunda,Terça,Quarta,Quinta,Sexta,Sábado,Domingo
@@ -697,3 +697,6 @@ label_user_activity: "Atividade de %s"
label_updated_time_by: Updated by %s %s ago
text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
setting_diff_max_lines_displayed: Max number of diff lines displayed
text_plugin_assets_writable: Plugin assets directory writable
warning_attachments_not_saved: "%d file(s) could not be saved."
button_create_and_continue: Create and continue

View File

@@ -698,3 +698,6 @@ label_user_activity: "Actividade de %s"
label_updated_time_by: Actualizado por %s há %s
text_diff_truncated: '... Este diff foi truncado porque excede o tamanho máximo que pode ser mostrado.'
setting_diff_max_lines_displayed: Número máximo de linhas de diff mostradas
text_plugin_assets_writable: Plugin assets directory writable
warning_attachments_not_saved: "%d file(s) could not be saved."
button_create_and_continue: Create and continue

View File

@@ -696,3 +696,6 @@ label_user_activity: "%s's activity"
label_updated_time_by: Updated by %s %s ago
text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
setting_diff_max_lines_displayed: Max number of diff lines displayed
text_plugin_assets_writable: Plugin assets directory writable
warning_attachments_not_saved: "%d file(s) could not be saved."
button_create_and_continue: Create and continue

View File

@@ -730,3 +730,6 @@ text_user_wrote: '%s написал(а):'
text_wiki_destroy_confirmation: Вы уверены, что хотите удалить данную Wiki и все ее содержимое?
text_workflow_edit: Выберите роль и трекер для редактирования последовательности состояний
text_plugin_assets_writable: Plugin assets directory writable
warning_attachments_not_saved: "%d file(s) could not be saved."
button_create_and_continue: Create and continue

View File

@@ -702,3 +702,6 @@ label_user_activity: "%s's activity"
label_updated_time_by: Updated by %s %s ago
text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
setting_diff_max_lines_displayed: Max number of diff lines displayed
text_plugin_assets_writable: Plugin assets directory writable
warning_attachments_not_saved: "%d file(s) could not be saved."
button_create_and_continue: Create and continue

View File

@@ -697,3 +697,6 @@ label_user_activity: "%s's activity"
label_updated_time_by: Updated by %s %s ago
text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
setting_diff_max_lines_displayed: Max number of diff lines displayed
text_plugin_assets_writable: Plugin assets directory writable
warning_attachments_not_saved: "%d file(s) could not be saved."
button_create_and_continue: Create and continue

View File

@@ -696,4 +696,7 @@ default_activity_development: Utveckling
enumeration_issue_priorities: Ärendeprioriteter
enumeration_doc_categories: Dokumentkategorier
enumeration_activities: Aktiviteter (tidsuppföljning)
enumeration_activities: Aktiviteter (tidsuppföljning)
text_plugin_assets_writable: Plugin assets directory writable
warning_attachments_not_saved: "%d file(s) could not be saved."
button_create_and_continue: Create and continue

View File

@@ -699,3 +699,6 @@ label_user_activity: "%s's activity"
label_updated_time_by: Updated by %s %s ago
text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
setting_diff_max_lines_displayed: Max number of diff lines displayed
text_plugin_assets_writable: Plugin assets directory writable
warning_attachments_not_saved: "%d file(s) could not be saved."
button_create_and_continue: Create and continue

View File

@@ -697,3 +697,6 @@ label_user_activity: "%s's activity"
label_updated_time_by: Updated by %s %s ago
text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
setting_diff_max_lines_displayed: Max number of diff lines displayed
text_plugin_assets_writable: Plugin assets directory writable
warning_attachments_not_saved: "%d file(s) could not be saved."
button_create_and_continue: Create and continue

View File

@@ -698,3 +698,6 @@ label_user_activity: "%s's activity"
label_updated_time_by: Updated by %s %s ago
text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
setting_diff_max_lines_displayed: Max number of diff lines displayed
text_plugin_assets_writable: Plugin assets directory writable
warning_attachments_not_saved: "%d file(s) could not be saved."
button_create_and_continue: Create and continue

View File

@@ -699,3 +699,6 @@ label_user_activity: "%s's activity"
label_updated_time_by: Updated by %s %s ago
text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
setting_diff_max_lines_displayed: Max number of diff lines displayed
text_plugin_assets_writable: Plugin assets directory writable
warning_attachments_not_saved: "%d file(s) could not be saved."
button_create_and_continue: Create and continue

View File

@@ -698,3 +698,6 @@ default_activity_development: 開發
enumeration_issue_priorities: 項目優先權
enumeration_doc_categories: 文件分類
enumeration_activities: 活動 (時間追蹤)
text_plugin_assets_writable: Plugin assets directory writable
warning_attachments_not_saved: "%d file(s) could not be saved."
button_create_and_continue: Create and continue

View File

@@ -698,3 +698,6 @@ default_activity_development: 开发
enumeration_issue_priorities: 问题优先级
enumeration_doc_categories: 文档类别
enumeration_activities: 活动(时间跟踪)
text_plugin_assets_writable: Plugin assets directory writable
warning_attachments_not_saved: "%d file(s) could not be saved."
button_create_and_continue: Create and continue

View File

@@ -791,7 +791,10 @@ class RedCloth3 < String
\s?
(?:\(([^)]+?)\)(?="))? # $title
":
([\w\/]\S+?) # $url
( # $url
(\/|[a-zA-Z]+:\/\/|www\.) # $proto
[\w\/]\S+?
)
(\/)? # $slash
([^\w\=\/;\(\)]*?) # $post
(?=<|\s|$)
@@ -799,7 +802,7 @@ class RedCloth3 < String
#"
def inline_textile_link( text )
text.gsub!( LINK_RE ) do |m|
pre,atts,text,title,url,slash,post = $~[1..7]
pre,atts,text,title,url,proto,slash,post = $~[1..8]
url, url_title = check_refs( url )
title ||= url_title

View File

@@ -35,7 +35,7 @@ Redmine::AccessControl.map do |map|
:queries => :index,
:reports => :issue_report}, :public => true
map.permission :add_issues, {:issues => :new}
map.permission :edit_issues, {:issues => [:edit, :reply, :bulk_edit, :destroy_attachment]}
map.permission :edit_issues, {:issues => [:edit, :reply, :bulk_edit]}
map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
map.permission :add_issue_notes, {:issues => [:edit, :reply]}
map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
@@ -67,12 +67,12 @@ Redmine::AccessControl.map do |map|
end
map.project_module :documents do |map|
map.permission :manage_documents, {:documents => [:new, :edit, :destroy, :add_attachment, :destroy_attachment]}, :require => :loggedin
map.permission :manage_documents, {:documents => [:new, :edit, :destroy, :add_attachment]}, :require => :loggedin
map.permission :view_documents, :documents => [:index, :show, :download]
end
map.project_module :files do |map|
map.permission :manage_files, {:projects => :add_file, :versions => :destroy_file}, :require => :loggedin
map.permission :manage_files, {:projects => :add_file}, :require => :loggedin
map.permission :view_files, :projects => :list_files, :versions => :download
end
@@ -83,7 +83,7 @@ Redmine::AccessControl.map do |map|
map.permission :view_wiki_pages, :wiki => [:index, :special]
map.permission :view_wiki_edits, :wiki => [:history, :diff, :annotate]
map.permission :edit_wiki_pages, :wiki => [:edit, :preview, :add_attachment]
map.permission :delete_wiki_pages_attachments, :wiki => :destroy_attachment
map.permission :delete_wiki_pages_attachments, {}
map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member
end

View File

@@ -24,7 +24,9 @@ module Redmine #:nodoc:
def to_hours
s = self.dup
s.strip!
unless s =~ %r{^[\d\.,]+$}
if s =~ %r{^(\d+([.,]\d+)?)h?$}
s = $1
else
# 2:30 => 2.5
s.gsub!(%r{^(\d+):(\d+)$}) { $1.to_i + $2.to_i / 60.0 }
# 2h30, 2h, 30m => 2.5, 2, 0.5

459
lib/redmine/export/pdf.rb Normal file
View File

@@ -0,0 +1,459 @@
# Redmine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'iconv'
require 'rfpdf/chinese'
module Redmine
module Export
module PDF
class IFPDF < FPDF
include GLoc
attr_accessor :footer_date
def initialize(lang)
super()
set_language_if_valid lang
case current_language.to_s
when 'ja'
extend(PDF_Japanese)
AddSJISFont()
@font_for_content = 'SJIS'
@font_for_footer = 'SJIS'
when 'zh'
extend(PDF_Chinese)
AddGBFont()
@font_for_content = 'GB'
@font_for_footer = 'GB'
when 'zh-tw'
extend(PDF_Chinese)
AddBig5Font()
@font_for_content = 'Big5'
@font_for_footer = 'Big5'
else
@font_for_content = 'Arial'
@font_for_footer = 'Helvetica'
end
SetCreator(Redmine::Info.app_name)
SetFont(@font_for_content)
end
def SetFontStyle(style, size)
SetFont(@font_for_content, style, size)
end
def SetTitle(txt)
txt = begin
utf16txt = Iconv.conv('UTF-16BE', 'UTF-8', txt)
hextxt = "<FEFF" # FEFF is BOM
hextxt << utf16txt.unpack("C*").map {|x| sprintf("%02X",x) }.join
hextxt << ">"
rescue
txt
end || ''
super(txt)
end
def textstring(s)
# Format a text string
if s =~ /^</ # This means the string is hex-dumped.
return s
else
return '('+escape(s)+')'
end
end
def Cell(w,h=0,txt='',border=0,ln=0,align='',fill=0,link='')
@ic ||= Iconv.new(l(:general_pdf_encoding), 'UTF-8')
# these quotation marks are not correctly rendered in the pdf
txt = txt.gsub(/[“â€<C3A2>]/, '"') if txt
txt = begin
# 0x5c char handling
txtar = txt.split('\\')
txtar << '' if txt[-1] == ?\\
txtar.collect {|x| @ic.iconv(x)}.join('\\').gsub(/\\/, "\\\\\\\\")
rescue
txt
end || ''
super w,h,txt,border,ln,align,fill,link
end
def Footer
SetFont(@font_for_footer, 'I', 8)
SetY(-15)
SetX(15)
Cell(0, 5, @footer_date, 0, 0, 'L')
SetY(-15)
SetX(-30)
Cell(0, 5, PageNo().to_s + '/{nb}', 0, 0, 'C')
end
end
# Returns a PDF string of a list of issues
def issues_to_pdf(issues, project)
pdf = IFPDF.new(current_language)
title = project ? "#{project} - #{l(:label_issue_plural)}" : "#{l(:label_issue_plural)}"
pdf.SetTitle(title)
pdf.AliasNbPages
pdf.footer_date = format_date(Date.today)
pdf.AddPage("L")
row_height = 7
# title
pdf.SetFontStyle('B',11)
pdf.Cell(190,10, title)
pdf.Ln
# headers
pdf.SetFontStyle('B',10)
pdf.SetFillColor(230, 230, 230)
pdf.Cell(15, row_height, "#", 0, 0, 'L', 1)
pdf.Cell(30, row_height, l(:field_tracker), 0, 0, 'L', 1)
pdf.Cell(30, row_height, l(:field_status), 0, 0, 'L', 1)
pdf.Cell(30, row_height, l(:field_priority), 0, 0, 'L', 1)
pdf.Cell(40, row_height, l(:field_assigned_to), 0, 0, 'L', 1)
pdf.Cell(25, row_height, l(:field_updated_on), 0, 0, 'L', 1)
pdf.Cell(0, row_height, l(:field_subject), 0, 0, 'L', 1)
pdf.Line(10, pdf.GetY, 287, pdf.GetY)
pdf.Ln
pdf.Line(10, pdf.GetY, 287, pdf.GetY)
pdf.SetY(pdf.GetY() + 1)
# rows
pdf.SetFontStyle('',9)
pdf.SetFillColor(255, 255, 255)
issues.each do |issue|
pdf.Cell(15, row_height, issue.id.to_s, 0, 0, 'L', 1)
pdf.Cell(30, row_height, issue.tracker.name, 0, 0, 'L', 1)
pdf.Cell(30, row_height, issue.status.name, 0, 0, 'L', 1)
pdf.Cell(30, row_height, issue.priority.name, 0, 0, 'L', 1)
pdf.Cell(40, row_height, issue.assigned_to ? issue.assigned_to.to_s : '', 0, 0, 'L', 1)
pdf.Cell(25, row_height, format_date(issue.updated_on), 0, 0, 'L', 1)
pdf.MultiCell(0, row_height, (project == issue.project ? issue.subject : "#{issue.project} - #{issue.subject}"))
pdf.Line(10, pdf.GetY, 287, pdf.GetY)
pdf.SetY(pdf.GetY() + 1)
end
pdf.Output
end
# Returns a PDF string of a single issue
def issue_to_pdf(issue)
pdf = IFPDF.new(current_language)
pdf.SetTitle("#{issue.project} - ##{issue.tracker} #{issue.id}")
pdf.AliasNbPages
pdf.footer_date = format_date(Date.today)
pdf.AddPage
pdf.SetFontStyle('B',11)
pdf.Cell(190,10, "#{issue.project} - #{issue.tracker} # #{issue.id}: #{issue.subject}")
pdf.Ln
y0 = pdf.GetY
pdf.SetFontStyle('B',9)
pdf.Cell(35,5, l(:field_status) + ":","LT")
pdf.SetFontStyle('',9)
pdf.Cell(60,5, issue.status.to_s,"RT")
pdf.SetFontStyle('B',9)
pdf.Cell(35,5, l(:field_priority) + ":","LT")
pdf.SetFontStyle('',9)
pdf.Cell(60,5, issue.priority.to_s,"RT")
pdf.Ln
pdf.SetFontStyle('B',9)
pdf.Cell(35,5, l(:field_author) + ":","L")
pdf.SetFontStyle('',9)
pdf.Cell(60,5, issue.author.to_s,"R")
pdf.SetFontStyle('B',9)
pdf.Cell(35,5, l(:field_category) + ":","L")
pdf.SetFontStyle('',9)
pdf.Cell(60,5, issue.category.to_s,"R")
pdf.Ln
pdf.SetFontStyle('B',9)
pdf.Cell(35,5, l(:field_created_on) + ":","L")
pdf.SetFontStyle('',9)
pdf.Cell(60,5, format_date(issue.created_on),"R")
pdf.SetFontStyle('B',9)
pdf.Cell(35,5, l(:field_assigned_to) + ":","L")
pdf.SetFontStyle('',9)
pdf.Cell(60,5, issue.assigned_to.to_s,"R")
pdf.Ln
pdf.SetFontStyle('B',9)
pdf.Cell(35,5, l(:field_updated_on) + ":","LB")
pdf.SetFontStyle('',9)
pdf.Cell(60,5, format_date(issue.updated_on),"RB")
pdf.SetFontStyle('B',9)
pdf.Cell(35,5, l(:field_due_date) + ":","LB")
pdf.SetFontStyle('',9)
pdf.Cell(60,5, format_date(issue.due_date),"RB")
pdf.Ln
for custom_value in issue.custom_values
pdf.SetFontStyle('B',9)
pdf.Cell(35,5, custom_value.custom_field.name + ":","L")
pdf.SetFontStyle('',9)
pdf.MultiCell(155,5, (show_value custom_value),"R")
end
pdf.SetFontStyle('B',9)
pdf.Cell(35,5, l(:field_subject) + ":","LTB")
pdf.SetFontStyle('',9)
pdf.Cell(155,5, issue.subject,"RTB")
pdf.Ln
pdf.SetFontStyle('B',9)
pdf.Cell(35,5, l(:field_description) + ":")
pdf.SetFontStyle('',9)
pdf.MultiCell(155,5, @issue.description,"BR")
pdf.Line(pdf.GetX, y0, pdf.GetX, pdf.GetY)
pdf.Line(pdf.GetX, pdf.GetY, 170, pdf.GetY)
pdf.Ln
if issue.changesets.any? && User.current.allowed_to?(:view_changesets, issue.project)
pdf.SetFontStyle('B',9)
pdf.Cell(190,5, l(:label_associated_revisions), "B")
pdf.Ln
for changeset in issue.changesets
pdf.SetFontStyle('B',8)
pdf.Cell(190,5, format_time(changeset.committed_on) + " - " + changeset.author.to_s)
pdf.Ln
unless changeset.comments.blank?
pdf.SetFontStyle('',8)
pdf.MultiCell(190,5, changeset.comments)
end
pdf.Ln
end
end
pdf.SetFontStyle('B',9)
pdf.Cell(190,5, l(:label_history), "B")
pdf.Ln
for journal in issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
pdf.SetFontStyle('B',8)
pdf.Cell(190,5, format_time(journal.created_on) + " - " + journal.user.name)
pdf.Ln
pdf.SetFontStyle('I',8)
for detail in journal.details
pdf.Cell(190,5, "- " + show_detail(detail, true))
pdf.Ln
end
if journal.notes?
pdf.SetFontStyle('',8)
pdf.MultiCell(190,5, journal.notes)
end
pdf.Ln
end
if issue.attachments.any?
pdf.SetFontStyle('B',9)
pdf.Cell(190,5, l(:label_attachment_plural), "B")
pdf.Ln
for attachment in issue.attachments
pdf.SetFontStyle('',8)
pdf.Cell(80,5, attachment.filename)
pdf.Cell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
pdf.Cell(25,5, format_date(attachment.created_on),0,0,"R")
pdf.Cell(65,5, attachment.author.name,0,0,"R")
pdf.Ln
end
end
pdf.Output
end
# Returns a PDF string of a gantt chart
def gantt_to_pdf(gantt, project)
pdf = IFPDF.new(current_language)
pdf.SetTitle("#{l(:label_gantt)} #{project}")
pdf.AliasNbPages
pdf.footer_date = format_date(Date.today)
pdf.AddPage("L")
pdf.SetFontStyle('B',12)
pdf.SetX(15)
pdf.Cell(70, 20, project.to_s)
pdf.Ln
pdf.SetFontStyle('B',9)
subject_width = 70
header_heigth = 5
headers_heigth = header_heigth
show_weeks = false
show_days = false
if gantt.months < 7
show_weeks = true
headers_heigth = 2*header_heigth
if gantt.months < 3
show_days = true
headers_heigth = 3*header_heigth
end
end
g_width = 210
zoom = (g_width) / (gantt.date_to - gantt.date_from + 1)
g_height = 120
t_height = g_height + headers_heigth
y_start = pdf.GetY
# Months headers
month_f = gantt.date_from
left = subject_width
height = header_heigth
gantt.months.times do
width = ((month_f >> 1) - month_f) * zoom
pdf.SetY(y_start)
pdf.SetX(left)
pdf.Cell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C")
left = left + width
month_f = month_f >> 1
end
# Weeks headers
if show_weeks
left = subject_width
height = header_heigth
if gantt.date_from.cwday == 1
# gantt.date_from is monday
week_f = gantt.date_from
else
# find next monday after gantt.date_from
week_f = gantt.date_from + (7 - gantt.date_from.cwday + 1)
width = (7 - gantt.date_from.cwday + 1) * zoom-1
pdf.SetY(y_start + header_heigth)
pdf.SetX(left)
pdf.Cell(width + 1, height, "", "LTR")
left = left + width+1
end
while week_f <= gantt.date_to
width = (week_f + 6 <= gantt.date_to) ? 7 * zoom : (gantt.date_to - week_f + 1) * zoom
pdf.SetY(y_start + header_heigth)
pdf.SetX(left)
pdf.Cell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C")
left = left + width
week_f = week_f+7
end
end
# Days headers
if show_days
left = subject_width
height = header_heigth
wday = gantt.date_from.cwday
pdf.SetFontStyle('B',7)
(gantt.date_to - gantt.date_from + 1).to_i.times do
width = zoom
pdf.SetY(y_start + 2 * header_heigth)
pdf.SetX(left)
pdf.Cell(width, height, day_name(wday).first, "LTR", 0, "C")
left = left + width
wday = wday + 1
wday = 1 if wday > 7
end
end
pdf.SetY(y_start)
pdf.SetX(15)
pdf.Cell(subject_width+g_width-15, headers_heigth, "", 1)
# Tasks
top = headers_heigth + y_start
pdf.SetFontStyle('B',7)
gantt.events.each do |i|
pdf.SetY(top)
pdf.SetX(15)
if i.is_a? Issue
pdf.Cell(subject_width-15, 5, "#{i.tracker} #{i.id}: #{i.subject}".sub(/^(.{30}[^\s]*\s).*$/, '\1 (...)'), "LR")
else
pdf.Cell(subject_width-15, 5, "#{l(:label_version)}: #{i.name}", "LR")
end
pdf.SetY(top)
pdf.SetX(subject_width)
pdf.Cell(g_width, 5, "", "LR")
pdf.SetY(top+1.5)
if i.is_a? Issue
i_start_date = (i.start_date >= gantt.date_from ? i.start_date : gantt.date_from )
i_end_date = (i.due_before <= gantt.date_to ? i.due_before : gantt.date_to )
i_done_date = i.start_date + ((i.due_before - i.start_date+1)*i.done_ratio/100).floor
i_done_date = (i_done_date <= gantt.date_from ? gantt.date_from : i_done_date )
i_done_date = (i_done_date >= gantt.date_to ? gantt.date_to : i_done_date )
i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
i_left = ((i_start_date - gantt.date_from)*zoom)
i_width = ((i_end_date - i_start_date + 1)*zoom)
d_width = ((i_done_date - i_start_date)*zoom)
l_width = ((i_late_date - i_start_date+1)*zoom) if i_late_date
l_width ||= 0
pdf.SetX(subject_width + i_left)
pdf.SetFillColor(200,200,200)
pdf.Cell(i_width, 2, "", 0, 0, "", 1)
if l_width > 0
pdf.SetY(top+1.5)
pdf.SetX(subject_width + i_left)
pdf.SetFillColor(255,100,100)
pdf.Cell(l_width, 2, "", 0, 0, "", 1)
end
if d_width > 0
pdf.SetY(top+1.5)
pdf.SetX(subject_width + i_left)
pdf.SetFillColor(100,100,255)
pdf.Cell(d_width, 2, "", 0, 0, "", 1)
end
pdf.SetY(top+1.5)
pdf.SetX(subject_width + i_left + i_width)
pdf.Cell(30, 2, "#{i.status} #{i.done_ratio}%")
else
i_left = ((i.start_date - gantt.date_from)*zoom)
pdf.SetX(subject_width + i_left)
pdf.SetFillColor(50,200,50)
pdf.Cell(2, 2, "", 0, 0, "", 1)
pdf.SetY(top+1.5)
pdf.SetX(subject_width + i_left + 3)
pdf.Cell(30, 2, "#{i.name}")
end
top = top + 5
pdf.SetDrawColor(200, 200, 200)
pdf.Line(15, top, subject_width+g_width, top)
if pdf.GetY() > 180
pdf.AddPage("L")
top = 20
pdf.Line(15, top, subject_width+g_width, top)
end
pdf.SetDrawColor(0, 0, 0)
end
pdf.Line(15, top, subject_width+g_width, top)
pdf.Output
end
end
end
end

View File

@@ -1,4 +1,4 @@
# redMine - project management software
# Redmine - project management software
# Copyright (C) 2006-2008 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
@@ -33,9 +33,18 @@ module Redmine
msg = imap.fetch(message_id,'RFC822')[0].attr['RFC822']
logger.debug "Receiving message #{message_id}" if logger && logger.debug?
if MailHandler.receive(msg, options)
logger.debug "Message #{message_id} successfully received" if logger && logger.debug?
if imap_options[:move_on_success]
imap.copy(message_id, imap_options[:move_on_success])
end
imap.store(message_id, "+FLAGS", [:Seen, :Deleted])
else
logger.debug "Message #{message_id} can not be processed" if logger && logger.debug?
imap.store(message_id, "+FLAGS", [:Seen])
if imap_options[:move_on_failure]
imap.copy(message_id, imap_options[:move_on_failure])
imap.store(message_id, "+FLAGS", [:Deleted])
end
end
end
imap.expunge

View File

@@ -52,8 +52,19 @@ module Redmine
# Returns the menu item name according to the current action
def current_menu_item
menu_items[controller_name.to_sym][:actions][action_name.to_sym] ||
menu_items[controller_name.to_sym][:default]
@current_menu_item ||= menu_items[controller_name.to_sym][:actions][action_name.to_sym] ||
menu_items[controller_name.to_sym][:default]
end
# Redirects user to the menu item of the given project
# Returns false if user is not authorized
def redirect_to_project_menu_item(project, name)
item = Redmine::MenuManager.items(:project_menu).detect {|i| i.name.to_s == name.to_s}
if item && User.current.allowed_to?(item.url, project) && (item.condition.nil? || item.condition.call(project))
redirect_to({item.param => project}.merge(item.url))
return true
end
false
end
end

View File

@@ -67,8 +67,8 @@ module Redmine
path = '.' if path.blank?
entries = Entries.new
cmd = "#{DARCS_BIN} annotate --repodir #{@url} --xml-output"
cmd << " --match \"hash #{identifier}\"" if identifier
cmd << " #{path}"
cmd << " --match #{shell_quote("hash #{identifier}")}" if identifier
cmd << " #{shell_quote path}"
shellout(cmd) do |io|
begin
doc = REXML::Document.new(io)
@@ -84,14 +84,14 @@ module Redmine
end
end
return nil if $? && $?.exitstatus != 0
entries.sort_by_name
entries.compact.sort_by_name
end
def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
path = '.' if path.blank?
revisions = Revisions.new
cmd = "#{DARCS_BIN} changes --repodir #{@url} --xml-output"
cmd << " --from-match \"hash #{identifier_from}\"" if identifier_from
cmd << " --from-match #{shell_quote("hash #{identifier_from}")}" if identifier_from
cmd << " --last #{options[:limit].to_i}" if options[:limit]
shellout(cmd) do |io|
begin
@@ -118,12 +118,12 @@ module Redmine
path = '*' if path.blank?
cmd = "#{DARCS_BIN} diff --repodir #{@url}"
if identifier_to.nil?
cmd << " --match \"hash #{identifier_from}\""
cmd << " --match #{shell_quote("hash #{identifier_from}")}"
else
cmd << " --to-match \"hash #{identifier_from}\""
cmd << " --from-match \"hash #{identifier_to}\""
cmd << " --to-match #{shell_quote("hash #{identifier_from}")}"
cmd << " --from-match #{shell_quote("hash #{identifier_to}")}"
end
cmd << " -u #{path}"
cmd << " -u #{shell_quote path}"
diff = []
shellout(cmd) do |io|
io.each_line do |line|
@@ -136,7 +136,7 @@ module Redmine
def cat(path, identifier=nil)
cmd = "#{DARCS_BIN} show content --repodir #{@url}"
cmd << " --match \"hash #{identifier}\"" if identifier
cmd << " --match #{shell_quote("hash #{identifier}")}" if identifier
cmd << " #{shell_quote path}"
cat = nil
shellout(cmd) do |io|
@@ -148,15 +148,22 @@ module Redmine
end
private
# Returns an Entry from the given XML element
# or nil if the entry was deleted
def entry_from_xml(element, path_prefix)
modified_element = element.elements['modified']
if modified_element.elements['modified_how'].text.match(/removed/)
return nil
end
Entry.new({:name => element.attributes['name'],
:path => path_prefix + element.attributes['name'],
:kind => element.name == 'file' ? 'file' : 'dir',
:size => nil,
:lastrev => Revision.new({
:identifier => nil,
:scmid => element.elements['modified'].elements['patch'].attributes['hash']
:scmid => modified_element.elements['patch'].attributes['hash']
})
})
end
@@ -164,7 +171,7 @@ module Redmine
# Retrieve changed paths for a single patch
def get_paths_for_patch(hash)
cmd = "#{DARCS_BIN} annotate --repodir #{@url} --summary --xml-output"
cmd << " --match \"hash #{hash}\" "
cmd << " --match #{shell_quote("hash #{hash}")} "
paths = []
shellout(cmd) do |io|
begin

View File

@@ -4,7 +4,7 @@ module Redmine
module VERSION #:nodoc:
MAJOR = 0
MINOR = 8
TINY = 0
TINY = 1
# Branch values:
# * official release: nil

View File

@@ -1,4 +1,4 @@
# redMine - project management software
# Redmine - project management software
# Copyright (C) 2006-2008 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
@@ -71,6 +71,11 @@ Issue attributes control options:
allow_override=ATTRS allow email content to override attributes
specified by previous options
ATTRS is a comma separated list of attributes
Processed emails control options:
move_on_success=MAILBOX move emails that were successfully received
to MAILBOX instead of deleting them
move_on_failure=MAILBOX move emails that were ignored to MAILBOX
Examples:
# No project specified. Emails MUST contain the 'Project' keyword:
@@ -95,7 +100,9 @@ END_DESC
:ssl => ENV['ssl'],
:username => ENV['username'],
:password => ENV['password'],
:folder => ENV['folder']}
:folder => ENV['folder'],
:move_on_success => ENV['move_on_success'],
:move_on_failure => ENV['move_on_failure']}
options = { :issue => {} }
%w(project status tracker category priority).each { |a| options[:issue][a.to_sym] = ENV[a] if ENV[a] }

View File

@@ -5,12 +5,12 @@
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
@@ -22,10 +22,10 @@ require 'pp'
namespace :redmine do
desc 'Trac migration script'
task :migrate_from_trac => :environment do
module TracMigrate
TICKET_MAP = []
DEFAULT_STATUS = IssueStatus.default
assigned_status = IssueStatus.find_by_position(2)
resolved_status = IssueStatus.find_by_position(3)
@@ -36,7 +36,7 @@ namespace :redmine do
'assigned' => assigned_status,
'closed' => closed_status
}
priorities = Enumeration.get_values('IPRI')
DEFAULT_PRIORITY = priorities[0]
PRIORITY_MAPPING = {'lowest' => priorities[0],
@@ -51,7 +51,7 @@ namespace :redmine do
'critical' => priorities[3],
'blocker' => priorities[4]
}
TRACKER_BUG = Tracker.find_by_position(1)
TRACKER_FEATURE = Tracker.find_by_position(2)
DEFAULT_TRACKER = TRACKER_BUG
@@ -60,7 +60,7 @@ namespace :redmine do
'task' => TRACKER_FEATURE,
'patch' =>TRACKER_FEATURE
}
roles = Role.find(:all, :conditions => {:builtin => 0}, :order => 'position ASC')
manager_role = roles[0]
developer_role = roles[1]
@@ -68,7 +68,7 @@ namespace :redmine do
ROLE_MAPPING = {'admin' => manager_role,
'developer' => developer_role
}
class ::Time
class << self
alias :real_now :now
@@ -87,10 +87,10 @@ namespace :redmine do
class TracComponent < ActiveRecord::Base
set_table_name :component
end
class TracMilestone < ActiveRecord::Base
set_table_name :milestone
# If this attribute is set a milestone has a defined target timepoint
# If this attribute is set a milestone has a defined target timepoint
def due
if read_attribute(:due) && read_attribute(:due) > 0
Time.at(read_attribute(:due)).to_date
@@ -112,37 +112,37 @@ namespace :redmine do
has_attribute?(:descr) ? read_attribute(:descr) : read_attribute(:description)
end
end
class TracTicketCustom < ActiveRecord::Base
set_table_name :ticket_custom
end
class TracAttachment < ActiveRecord::Base
set_table_name :attachment
set_inheritance_column :none
def time; Time.at(read_attribute(:time)) end
def original_filename
filename
end
def content_type
Redmine::MimeType.of(filename) || ''
end
def exist?
File.file? trac_fullpath
end
def read
File.open("#{trac_fullpath}", 'rb').read
end
def description
read_attribute(:description).to_s.slice(0,255)
end
private
def trac_fullpath
attachment_type = read_attribute(:type)
@@ -150,11 +150,11 @@ namespace :redmine do
"#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}"
end
end
class TracTicket < ActiveRecord::Base
set_table_name :ticket
set_inheritance_column :none
# ticket changes: only migrate status changes and comments
has_many :changes, :class_name => "TracTicketChange", :foreign_key => :ticket
has_many :attachments, :class_name => "TracAttachment",
@@ -162,29 +162,29 @@ namespace :redmine do
" WHERE #{TracMigrate::TracAttachment.table_name}.type = 'ticket'" +
' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{id}\''
has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
def ticket_type
read_attribute(:type)
end
def summary
read_attribute(:summary).blank? ? "(no subject)" : read_attribute(:summary)
end
def description
read_attribute(:description).blank? ? summary : read_attribute(:description)
end
def time; Time.at(read_attribute(:time)) end
def changetime; Time.at(read_attribute(:changetime)) end
end
class TracTicketChange < ActiveRecord::Base
set_table_name :ticket_change
def time; Time.at(read_attribute(:time)) end
end
TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
@@ -192,35 +192,35 @@ namespace :redmine do
TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
CamelCase TitleIndex)
class TracWikiPage < ActiveRecord::Base
set_table_name :wiki
set_primary_key :name
has_many :attachments, :class_name => "TracAttachment",
:finder_sql => "SELECT DISTINCT attachment.* FROM #{TracMigrate::TracAttachment.table_name}" +
" WHERE #{TracMigrate::TracAttachment.table_name}.type = 'wiki'" +
' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{id}\''
def self.columns
# Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0)
super.select {|column| column.name.to_s != 'readonly'}
end
def time; Time.at(read_attribute(:time)) end
end
class TracPermission < ActiveRecord::Base
set_table_name :permission
set_table_name :permission
end
class TracSessionAttribute < ActiveRecord::Base
set_table_name :session_attribute
end
def self.find_or_create_user(username, project_member = false)
return User.anonymous if username.blank?
u = User.find_by_login(username)
if !u
# Create a new user if not found
@@ -229,7 +229,7 @@ namespace :redmine do
mail = mail_attr.value
end
mail = "#{mail}@foo.bar" unless mail.include?("@")
name = username
if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name')
name = name_attr.value
@@ -237,7 +237,7 @@ namespace :redmine do
name =~ (/(.*)(\s+\w+)?/)
fn = $1.strip
ln = ($2 || '-').strip
u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'),
:firstname => fn[0, limit_for(User, 'firstname')].gsub(/[^\w\s\'\-]/i, '-'),
:lastname => ln[0, limit_for(User, 'lastname')].gsub(/[^\w\s\'\-]/i, '-')
@@ -261,7 +261,7 @@ namespace :redmine do
end
u
end
# Basic wiki syntax conversion
def self.convert_wiki_text(text)
# Titles
@@ -282,7 +282,7 @@ namespace :redmine do
# [milestone:"0.1.0 Mercury"]
text = text.gsub(/\[milestone\:\"([^\"]+)\"\]/, 'version:"\1"')
text = text.gsub(/milestone\:\"([^\"]+)\"/, 'version:"\1"')
# milestone:0.1.0
# milestone:0.1.0
text = text.gsub(/\[milestone\:([^\ ]+)\]/, 'version:\1')
text = text.gsub(/milestone\:([^\ ]+)/, 'version:\1')
# Internal Links
@@ -293,11 +293,11 @@ namespace :redmine do
text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"}
# Links to pages UsingJustWikiCaps
text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
# Normalize things that were supposed to not be links
# like !NotALink
text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
# Links to pages UsingJustWikiCaps
text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
# Normalize things that were supposed to not be links
# like !NotALink
text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
# Revisions links
text = text.gsub(/\[(\d+)\]/, 'r\1')
# Ticket number re-writing
@@ -318,7 +318,7 @@ namespace :redmine do
shebang_re = /^\#\!([a-z]+)/
# Regular expression for end of code
pre_end_re = /\}\}\}/
# Go through the whole text..extract it line by line
text = text.gsub(/^(.*)$/) do |line|
m_pre = pre_re.match(line)
@@ -338,7 +338,7 @@ namespace :redmine do
end
end
end
line
line
end
# Highlighting
@@ -349,25 +349,25 @@ namespace :redmine do
text = text.gsub(/__/, '+')
text = text.gsub(/~~/, '-')
text = text.gsub(/`/, '@')
text = text.gsub(/,,/, '~')
text = text.gsub(/,,/, '~')
# Lists
text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
text
end
def self.migrate
establish_connection
# Quick database test
TracComponent.count
migrated_components = 0
migrated_milestones = 0
migrated_tickets = 0
migrated_custom_values = 0
migrated_ticket_attachments = 0
migrated_wiki_edits = 0
migrated_wiki_edits = 0
migrated_wiki_attachments = 0
#Wiki system initializing...
@@ -375,21 +375,21 @@ namespace :redmine do
@target_project.reload
wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
wiki_edit_count = 0
# Components
print "Migrating components"
issues_category_map = {}
TracComponent.find(:all).each do |component|
print '.'
STDOUT.flush
print '.'
STDOUT.flush
c = IssueCategory.new :project => @target_project,
:name => encode(component.name[0, limit_for(IssueCategory, 'name')])
next unless c.save
issues_category_map[component.name] = c
migrated_components += 1
next unless c.save
issues_category_map[component.name] = c
migrated_components += 1
end
puts
# Milestones
print "Migrating milestones"
version_map = {}
@@ -415,7 +415,7 @@ namespace :redmine do
migrated_milestones += 1
end
puts
# Custom fields
# TODO: read trac.ini instead
print "Migrating custom fields"
@@ -430,14 +430,14 @@ namespace :redmine do
# Or create a new one
f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
:field_format => 'string')
next if f.new_record?
f.trackers = Tracker.find(:all)
f.projects << @target_project
custom_field_map[field.name] = f
end
puts
# Trac 'resolution' field as a Redmine custom field
r = IssueCustomField.find(:first, :conditions => { :name => "Resolution" })
r = IssueCustomField.new(:name => 'Resolution',
@@ -448,45 +448,44 @@ namespace :redmine do
r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq
r.save!
custom_field_map['resolution'] = r
# Tickets
print "Migrating tickets"
TracTicket.find(:all, :order => 'id ASC').each do |ticket|
print '.'
STDOUT.flush
i = Issue.new :project => @target_project,
print '.'
STDOUT.flush
i = Issue.new :project => @target_project,
:subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
:description => convert_wiki_text(encode(ticket.description)),
:priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
:created_on => ticket.time
i.author = find_or_create_user(ticket.reporter)
i.category = issues_category_map[ticket.component] unless ticket.component.blank?
i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
i.custom_values << CustomValue.new(:custom_field => custom_field_map['resolution'], :value => ticket.resolution) unless ticket.resolution.blank?
i.id = ticket.id unless Issue.exists?(ticket.id)
next unless Time.fake(ticket.changetime) { i.save }
TICKET_MAP[ticket.id] = i.id
migrated_tickets += 1
# Owner
i.author = find_or_create_user(ticket.reporter)
i.category = issues_category_map[ticket.component] unless ticket.component.blank?
i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
i.id = ticket.id unless Issue.exists?(ticket.id)
next unless Time.fake(ticket.changetime) { i.save }
TICKET_MAP[ticket.id] = i.id
migrated_tickets += 1
# Owner
unless ticket.owner.blank?
i.assigned_to = find_or_create_user(ticket.owner, true)
Time.fake(ticket.changetime) { i.save }
end
# Comments and status/resolution changes
ticket.changes.group_by(&:time).each do |time, changeset|
# Comments and status/resolution changes
ticket.changes.group_by(&:time).each do |time, changeset|
status_change = changeset.select {|change| change.field == 'status'}.first
resolution_change = changeset.select {|change| change.field == 'resolution'}.first
comment_change = changeset.select {|change| change.field == 'comment'}.first
n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
:created_on => time
n.user = find_or_create_user(changeset.first.author)
n.journalized = i
if status_change &&
if status_change &&
STATUS_MAPPING[status_change.oldvalue] &&
STATUS_MAPPING[status_change.newvalue] &&
(STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
@@ -502,35 +501,39 @@ namespace :redmine do
:value => resolution_change.newvalue)
end
n.save unless n.details.empty? && n.notes.blank?
end
# Attachments
ticket.attachments.each do |attachment|
next unless attachment.exist?
end
# Attachments
ticket.attachments.each do |attachment|
next unless attachment.exist?
a = Attachment.new :created_on => attachment.time
a.file = attachment
a.author = find_or_create_user(attachment.author)
a.container = i
a.description = attachment.description
migrated_ticket_attachments += 1 if a.save
end
# Custom fields
ticket.customs.each do |custom|
next if custom_field_map[custom.name].nil?
v = CustomValue.new :custom_field => custom_field_map[custom.name],
:value => custom.value
v.customized = i
next unless v.save
end
# Custom fields
custom_values = ticket.customs.inject({}) do |h, custom|
if custom_field = custom_field_map[custom.name]
h[custom_field.id] = custom.value
migrated_custom_values += 1
end
end
h
end
if custom_field_map['resolution'] && !ticket.resolution.blank?
custom_values[custom_field_map['resolution'].id] = ticket.resolution
end
i.custom_field_values = custom_values
i.save_custom_field_values
end
# update issue id sequence if needed (postgresql)
Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
puts
# Wiki
# Wiki
print "Migrating wiki"
if wiki.save
TracWikiPage.find(:all, :order => 'name, version').each do |page|
@@ -545,10 +548,10 @@ namespace :redmine do
p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac'
p.content.comments = page.comment
Time.fake(page.time) { p.new_record? ? p.save : p.content.save }
next if p.content.new_record?
migrated_wiki_edits += 1
migrated_wiki_edits += 1
# Attachments
page.attachments.each do |attachment|
next unless attachment.exist?
@@ -561,7 +564,7 @@ namespace :redmine do
migrated_wiki_attachments += 1 if a.save
end
end
wiki.reload
wiki.pages.each do |page|
page.content.text = convert_wiki_text(page.content.text)
@@ -569,7 +572,7 @@ namespace :redmine do
end
end
puts
puts
puts "Components: #{migrated_components}/#{TracComponent.count}"
puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}"
@@ -579,18 +582,18 @@ namespace :redmine do
puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}"
puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
end
def self.limit_for(klass, attribute)
klass.columns_hash[attribute.to_s].limit
end
def self.encoding(charset)
@ic = Iconv.new('UTF-8', charset)
rescue Iconv::InvalidEncoding
puts "Invalid encoding!"
return false
end
def self.set_trac_directory(path)
@@trac_directory = path
raise "This directory doesn't exist!" unless File.directory?(path)
@@ -615,7 +618,7 @@ namespace :redmine do
puts e
return false
end
def self.set_trac_db_host(host)
return nil if host.blank?
@@trac_db_host = host
@@ -625,7 +628,7 @@ namespace :redmine do
return nil if port.to_i == 0
@@trac_db_port = port.to_i
end
def self.set_trac_db_name(name)
return nil if name.blank?
@@trac_db_name = name
@@ -634,22 +637,22 @@ namespace :redmine do
def self.set_trac_db_username(username)
@@trac_db_username = username
end
def self.set_trac_db_password(password)
@@trac_db_password = password
end
def self.set_trac_db_schema(schema)
@@trac_db_schema = schema
end
mattr_reader :trac_directory, :trac_adapter, :trac_db_host, :trac_db_port, :trac_db_name, :trac_db_schema, :trac_db_username, :trac_db_password
def self.trac_db_path; "#{trac_directory}/db/trac.db" end
def self.trac_attachments_directory; "#{trac_directory}/attachments" end
def self.target_project_identifier(identifier)
project = Project.find_by_identifier(identifier)
project = Project.find_by_identifier(identifier)
if !project
# create the target project
project = Project.new :name => identifier.humanize,
@@ -662,16 +665,16 @@ namespace :redmine do
puts
puts "This project already exists in your Redmine database."
print "Are you sure you want to append data to this project ? [Y/n] "
exit if STDIN.gets.match(/^n$/i)
exit if STDIN.gets.match(/^n$/i)
end
project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
@target_project = project.new_record? ? nil : project
end
def self.connection_params
if %w(sqlite sqlite3).include?(trac_adapter)
{:adapter => trac_adapter,
{:adapter => trac_adapter,
:database => trac_db_path}
else
{:adapter => trac_adapter,
@@ -684,7 +687,7 @@ namespace :redmine do
}
end
end
def self.establish_connection
constants.each do |const|
klass = const_get(const)
@@ -692,7 +695,7 @@ namespace :redmine do
klass.establish_connection connection_params
end
end
private
def self.encode(text)
@ic.iconv text
@@ -700,7 +703,7 @@ namespace :redmine do
text
end
end
puts
if Redmine::DefaultData::Loader.no_data?
puts "Redmine configuration need to be loaded before importing data."
@@ -709,10 +712,10 @@ namespace :redmine do
puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
exit
end
puts "WARNING: a new project will be added to Redmine during this process."
print "Are you sure you want to continue ? [y/N] "
break unless STDIN.gets.match(/^y$/i)
break unless STDIN.gets.match(/^y$/i)
puts
def prompt(text, options = {}, &block)
@@ -724,9 +727,9 @@ namespace :redmine do
break if yield value
end
end
DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
prompt('Trac database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite') {|adapter| TracMigrate.set_trac_adapter adapter}
unless %w(sqlite sqlite3).include?(TracMigrate.trac_adapter)
@@ -740,7 +743,7 @@ namespace :redmine do
prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
puts
TracMigrate.migrate
end
end

View File

@@ -61,7 +61,7 @@
<ul>
<li><strong>[[sandbox:some page]]</strong> displays a link to the page named 'Some page' of the Sandbox wiki</li>
<li><strong>[[sandbox]]</strong> displays a link to the Sandbox wiki main page</li>
<li><strong>[[sandbox:]]</strong> displays a link to the Sandbox wiki main page</li>
</ul>
<p>Wiki links are displayed in red if the page doesn't exist yet, eg: <a href="Nonexistent_page.html" class="wiki-page new">Nonexistent page</a>.</p>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 535 B

After

Width:  |  Height:  |  Size: 666 B

View File

@@ -314,6 +314,14 @@ div.flash.notice {
color: #005f00;
}
div.flash.warning {
background: url(../images/warning.png) 8px 5px no-repeat;
background-color: #FFEBC1;
border-color: #FDBF3B;
color: #A6750C;
text-align: left;
}
.nodata, .warning {
text-align: center;
background-color: #FFEBC1;

View File

@@ -85,4 +85,28 @@ attachments_007:
filename: archive.zip
author_id: 1
content_type: application/octet-stream
attachments_008:
created_on: 2006-07-19 21:07:27 +02:00
container_type: Project
container_id: 1
downloads: 0
disk_filename: 060719210727_project_file.zip
digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
id: 8
filesize: 320
filename: project_file.zip
author_id: 2
content_type: application/octet-stream
attachments_009:
created_on: 2006-07-19 21:07:27 +02:00
container_type: Version
container_id: 1
downloads: 0
disk_filename: 060719210727_version_file.zip
digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
id: 9
filesize: 452
filename: version_file.zip
author_id: 2
content_type: application/octet-stream

Some files were not shown because too many files have changed in this diff Show More