# HG changeset patch
# User Chris Cannam
# Date 1389710262 0
# Node ID 261b3d9a4903c28d2b582f13ef5035ad20ba9566
# Parent 038ba2d95de864d8388e537f66ed1929fdf49c93
Update to Redmine 2.4 branch rev 12663
diff -r 038ba2d95de8 -r 261b3d9a4903 .gitignore
--- a/.gitignore Fri Jun 14 09:05:06 2013 +0100
+++ b/.gitignore Tue Jan 14 14:37:42 2014 +0000
@@ -1,5 +1,7 @@
/.project
/.loadpath
+/.powrc
+/.rvmrc
/config/additional_environment.rb
/config/configuration.yml
/config/database.yml
@@ -15,8 +17,14 @@
/lib/redmine/scm/adapters/mercurial/redminehelper.pyo
/log/*.log*
/log/mongrel_debug
+/plugins/*
+!/plugins/README
/public/dispatch.*
/public/plugin_assets
+/public/themes/*
+!/public/themes/alternate
+!/public/themes/classic
+!/public/themes/README
/tmp/*
/tmp/cache/*
/tmp/pdf/*
diff -r 038ba2d95de8 -r 261b3d9a4903 .hgignore
--- a/.hgignore Fri Jun 14 09:05:06 2013 +0100
+++ b/.hgignore Tue Jan 14 14:37:42 2014 +0000
@@ -2,6 +2,8 @@
.project
.loadpath
+.powrc
+.rvmrc
config/additional_environment.rb
config/configuration.yml
config/database.yml
diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/entries
--- a/.svn/entries Fri Jun 14 09:05:06 2013 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-12
diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/format
--- a/.svn/format Fri Jun 14 09:05:06 2013 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-12
diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/00/0009a621965fc195e93ba67a7d3500cbcd38b084.svn-base
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/.svn/pristine/00/0009a621965fc195e93ba67a7d3500cbcd38b084.svn-base Tue Jan 14 14:37:42 2014 +0000
@@ -0,0 +1,94 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class DocumentsController < ApplicationController
+ default_search_scope :documents
+ model_object Document
+ before_filter :find_project_by_project_id, :only => [:index, :new, :create]
+ before_filter :find_model_object, :except => [:index, :new, :create]
+ before_filter :find_project_from_association, :except => [:index, :new, :create]
+ before_filter :authorize
+
+ helper :attachments
+
+ def index
+ @sort_by = %w(category date title author).include?(params[:sort_by]) ? params[:sort_by] : 'category'
+ documents = @project.documents.includes(:attachments, :category).all
+ case @sort_by
+ when 'date'
+ @grouped = documents.group_by {|d| d.updated_on.to_date }
+ when 'title'
+ @grouped = documents.group_by {|d| d.title.first.upcase}
+ when 'author'
+ @grouped = documents.select{|d| d.attachments.any?}.group_by {|d| d.attachments.last.author}
+ else
+ @grouped = documents.group_by(&:category)
+ end
+ @document = @project.documents.build
+ render :layout => false if request.xhr?
+ end
+
+ def show
+ @attachments = @document.attachments.all
+ end
+
+ def new
+ @document = @project.documents.build
+ @document.safe_attributes = params[:document]
+ end
+
+ def create
+ @document = @project.documents.build
+ @document.safe_attributes = params[:document]
+ @document.save_attachments(params[:attachments])
+ if @document.save
+ render_attachment_warning_if_needed(@document)
+ flash[:notice] = l(:notice_successful_create)
+ redirect_to project_documents_path(@project)
+ else
+ render :action => 'new'
+ end
+ end
+
+ def edit
+ end
+
+ def update
+ @document.safe_attributes = params[:document]
+ if request.put? and @document.save
+ flash[:notice] = l(:notice_successful_update)
+ redirect_to document_path(@document)
+ else
+ render :action => 'edit'
+ end
+ end
+
+ def destroy
+ @document.destroy if request.delete?
+ redirect_to project_documents_path(@project)
+ end
+
+ def add_attachment
+ attachments = Attachment.attach_files(@document, params[:attachments])
+ render_attachment_warning_if_needed(@document)
+
+ if attachments.present? && attachments[:files].present? && Setting.notified_events.include?('document_added')
+ Mailer.attachments_added(attachments[:files]).deliver
+ end
+ redirect_to document_path(@document)
+ end
+end
diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/00/00205ce9aaa0cc98a4089319a62880b29732ab31.svn-base
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/.svn/pristine/00/00205ce9aaa0cc98a4089319a62880b29732ab31.svn-base Tue Jan 14 14:37:42 2014 +0000
@@ -0,0 +1,4349 @@
+#============================================================+
+# File name : tcpdf.rb
+# Begin : 2002-08-03
+# Last Update : 2007-03-20
+# Author : Nicola Asuni
+# Version : 1.53.0.TC031
+# License : GNU LGPL (http://www.gnu.org/copyleft/lesser.html)
+#
+# Description : This is a Ruby class for generating PDF files
+# on-the-fly without requiring external
+# extensions.
+#
+# IMPORTANT:
+# This class is an extension and improvement of the Public Domain
+# FPDF class by Olivier Plathey (http://www.fpdf.org).
+#
+# Main changes by Nicola Asuni:
+# Ruby porting;
+# UTF-8 Unicode support;
+# code refactoring;
+# source code clean up;
+# code style and formatting;
+# source code documentation using phpDocumentor (www.phpdoc.org);
+# All ISO page formats were included;
+# image scale factor;
+# includes methods to parse and printsome XHTML code, supporting the following elements: h1, h2, h3, h4, h5, h6, b, u, i, a, img, p, br, strong, em, font, blockquote, li, ul, ol, hr, td, th, tr, table, sup, sub, small;
+# includes a method to print various barcode formats using an improved version of "Generic Barcode Render Class" by Karim Mribti (http://www.mribti.com/barcode/) (require GD library: http://www.boutell.com/gd/);
+# defines standard Header() and Footer() methods.
+#
+# Ported to Ruby by Ed Moss 2007-08-06
+#
+#============================================================+
+
+require 'tempfile'
+require 'core/rmagick'
+
+#
+# TCPDF Class.
+# @package com.tecnick.tcpdf
+#
+
+PDF_PRODUCER = 'TCPDF via RFPDF 1.53.0.TC031 (http://tcpdf.sourceforge.net)'
+
+module TCPDFFontDescriptor
+ @@descriptors = { 'freesans' => {} }
+ @@font_name = 'freesans'
+
+ def self.font(font_name)
+ @@descriptors[font_name.gsub(".rb", "")]
+ end
+
+ def self.define(font_name = 'freesans')
+ @@descriptors[font_name] ||= {}
+ yield @@descriptors[font_name]
+ end
+end
+
+# This is a Ruby class for generating PDF files on-the-fly without requiring external extensions.
+# This class is an extension and improvement of the FPDF class by Olivier Plathey (http://www.fpdf.org).
+# This version contains some changes: [porting to Ruby, support for UTF-8 Unicode, code style and formatting, php documentation (www.phpdoc.org), ISO page formats, minor improvements, image scale factor]
+# TCPDF project (http://tcpdf.sourceforge.net) is based on the Public Domain FPDF class by Olivier Plathey (http://www.fpdf.org).
+# To add your own TTF fonts please read /fonts/README.TXT
+# @name TCPDF
+# @package com.tecnick.tcpdf
+# @@version 1.53.0.TC031
+# @author Nicola Asuni
+# @link http://tcpdf.sourceforge.net
+# @license http://www.gnu.org/copyleft/lesser.html LGPL
+#
+class TCPDF
+ include RFPDF
+ include Core::RFPDF
+ include RFPDF::Math
+
+ def logger
+ Rails.logger
+ end
+
+ @@version = "1.53.0.TC031"
+ @@fpdf_charwidths = {}
+
+ cattr_accessor :k_cell_height_ratio
+ @@k_cell_height_ratio = 1.25
+
+ cattr_accessor :k_blank_image
+ @@k_blank_image = ""
+
+ cattr_accessor :k_small_ratio
+ @@k_small_ratio = 2/3.0
+
+ cattr_accessor :k_path_cache
+ @@k_path_cache = Rails.root.join('tmp')
+
+ cattr_accessor :k_path_url_cache
+ @@k_path_url_cache = Rails.root.join('tmp')
+
+ attr_accessor :barcode
+
+ attr_accessor :buffer
+
+ attr_accessor :diffs
+
+ attr_accessor :color_flag
+
+ attr_accessor :default_table_columns
+
+ attr_accessor :max_table_columns
+
+ attr_accessor :default_font
+
+ attr_accessor :draw_color
+
+ attr_accessor :encoding
+
+ attr_accessor :fill_color
+
+ attr_accessor :fonts
+
+ attr_accessor :font_family
+
+ attr_accessor :font_files
+
+ cattr_accessor :font_path
+
+ attr_accessor :font_style
+
+ attr_accessor :font_size_pt
+
+ attr_accessor :header_width
+
+ attr_accessor :header_logo
+
+ attr_accessor :header_logo_width
+
+ attr_accessor :header_title
+
+ attr_accessor :header_string
+
+ attr_accessor :images
+
+ attr_accessor :img_scale
+
+ attr_accessor :in_footer
+
+ attr_accessor :is_unicode
+
+ attr_accessor :lasth
+
+ attr_accessor :links
+
+ attr_accessor :list_ordered
+
+ attr_accessor :list_count
+
+ attr_accessor :li_spacer
+
+ attr_accessor :n
+
+ attr_accessor :offsets
+
+ attr_accessor :orientation_changes
+
+ attr_accessor :page
+
+ attr_accessor :page_links
+
+ attr_accessor :pages
+
+ attr_accessor :pdf_version
+
+ attr_accessor :prevfill_color
+
+ attr_accessor :prevtext_color
+
+ attr_accessor :print_header
+
+ attr_accessor :print_footer
+
+ attr_accessor :state
+
+ attr_accessor :tableborder
+
+ attr_accessor :tdbegin
+
+ attr_accessor :tdwidth
+
+ attr_accessor :tdheight
+
+ attr_accessor :tdalign
+
+ attr_accessor :tdfill
+
+ attr_accessor :tempfontsize
+
+ attr_accessor :text_color
+
+ attr_accessor :underline
+
+ attr_accessor :ws
+
+ #
+ # This is the class constructor.
+ # It allows to set up the page format, the orientation and
+ # the measure unit used in all the methods (except for the font sizes).
+ # @since 1.0
+ # @param string :orientation page orientation. Possible values are (case insensitive):
+ # class PDF extends TCPDF { + # def Footer() + # #Go to 1.5 cm from bottom + # SetY(-15); + # #Select Arial italic 8 + # SetFont('Arial','I',8); + # #Print current and total page numbers + # Cell(0,10,'Page '.PageNo().'/{nb}',0,0,'C'); + # end + # } + # :pdf=new PDF(); + # :pdf->alias_nb_pages(); + #+ # @param string :alias The alias. Default valuenb}. + # @since 1.4 + # @see PageNo(), Footer() + # + def AliasNbPages(alias_nb ='{nb}') + #Define an alias for total number of pages + @alias_nb_pages = escapetext(alias_nb) + end + alias_method :alias_nb_pages, :AliasNbPages + + # + # This method is automatically called in case of fatal error; it simply outputs the message and halts the execution. An inherited class may override it to customize the error handling but should always halt the script, or the resulting document would probably be invalid. + # 2004-06-11 :: Nicola Asuni : changed bold tag with strong + # @param string :msg The error message + # @since 1.0 + # + def Error(msg) + #Fatal error + raise ("TCPDF error: #{msg}") + end + alias_method :error, :Error + + # + # This method begins the generation of the PDF document. It is not necessary to call it explicitly because AddPage() does it automatically. + # Note: no page is created by this method + # @since 1.0 + # @see AddPage(), Close() + # + def Open() + #Begin document + @state = 1 + end + # alias_method :open, :Open + + # + # Terminates the PDF document. It is not necessary to call this method explicitly because Output() does it automatically. If the document contains no page, AddPage() is called to prevent from getting an invalid document. + # @since 1.0 + # @see Open(), Output() + # + def Close() + #Terminate document + if (@state==3) + return; + end + if (@page==0) + AddPage(); + end + #Page footer + @in_footer=true; + Footer(); + @in_footer=false; + #Close page + endpage(); + #Close document + enddoc(); + end + # alias_method :close, :Close + + # + # Adds a new page to the document. If a page is already present, the Footer() method is called first to output the footer. Then the page is added, the current position set to the top-left corner according to the left and top margins, and Header() is called to display the header. + # The font which was set before calling is automatically restored. There is no need to call SetFont() again if you want to continue with the same font. The same is true for colors and line width. + # The origin of the coordinate system is at the top-left corner and increasing ordinates go downwards. + # @param string :orientation Page orientation. Possible values are (case insensitive):
+ # :pdf->AddFont('Comic','I'); + # # is equivalent to: + # :pdf->AddFont('Comic','I','comici.rb'); + #+ # @param string :family Font family. The name can be chosen arbitrarily. If it is a standard family name, it will override the corresponding font. + # @param string :style Font style. Possible values are (case insensitive):
+ # define('FPDF_FONTPATH','/home/www/font/'); + # require('tcpdf.rb'); + # + # #Times regular 12 + # :pdf->SetFont('Times'); + # #Arial bold 14 + # :pdf->SetFont('Arial','B',14); + # #Removes bold + # :pdf->SetFont(''); + # #Times bold, italic and underlined 14 + # :pdf->SetFont('Times','BIUD'); + #
+ # class PDF extends TCPDF { + # var :col=0; + # + # def SetCol(col) + # #Move position to a column + # @col = col; + # :x=10+:col*65; + # SetLeftMargin(x); + # SetX(x); + # end + # + # def AcceptPageBreak() + # if (@col<2) + # #Go to next column + # SetCol(@col+1); + # SetY(10); + # return false; + # end + # else + # #Go back to first column and issue page break + # SetCol(0); + # return true; + # end + # end + # } + # + # :pdf=new PDF(); + # :pdf->Open(); + # :pdf->AddPage(); + # :pdf->SetFont('Arial','',12); + # for(i=1;:i<=300;:i++) + # :pdf->Cell(0,5,"Line :i",0,1); + # } + # :pdf->Output(); + #+ # @return boolean + # @since 1.4 + # @see SetAutoPageBreak() + # + def AcceptPageBreak() + #Accept automatic page break or not + return @auto_page_break; + end + alias_method :accept_page_break, :AcceptPageBreak + + def BreakThePage?(h) + if ((@y + h) > @page_break_trigger and !@in_footer and AcceptPageBreak()) + true + else + false + end + end + alias_method :break_the_page?, :BreakThePage? + # + # Prints a cell (rectangular area) with optional borders, background color and character string. The upper-left corner of the cell corresponds to the current position. The text can be aligned or centered. After the call, the current position moves to the right or to the next line. It is possible to put a link on the text.
+ # #Begin with regular font + # :pdf->SetFont('Arial','',14); + # :pdf->Write(5,'Visit '); + # #Then put a blue underlined link + # :pdf->SetTextColor(0,0,255); + # :pdf->SetFont('','U'); + # :pdf->Write(5,'www.tecnick.com','http://www.tecnick.com'); + #+ # @param float :h Line height + # @param string :txt String to print + # @param mixed :link URL or identifier returned by AddLink() + # @param int :fill Indicates if the background must be painted (1) or transparent (0). Default value: 0. + # @since 1.5 + # @see SetFont(), SetTextColor(), AddLink(), MultiCell(), SetAutoPageBreak() + # + def Write(h, txt, link=nil, fill=0) + + #Output text in flowing mode + w = @w - @r_margin - @x; + wmax = (w - 3 * @c_margin); + + s = txt.gsub("\r", ''); + nb = s.length; + + # handle single space character + if ((nb==1) and (s == " ")) + @x += GetStringWidth(s); + return; + end + + sep=-1; + i=0; + j=0; + l=0; + nl=1; + while(i
+ # Char. number range | UTF-8 octet sequence + # (hexadecimal) | (binary) + # --------------------+----------------------------------------------- + # 0000 0000-0000 007F | 0xxxxxxx + # 0000 0080-0000 07FF | 110xxxxx 10xxxxxx + # 0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx + # 0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + # --------------------------------------------------------------------- + # + # ABFN notation: + # --------------------------------------------------------------------- + # UTF8-octets =#( UTF8-char ) + # UTF8-char = UTF8-1 / UTF8-2 / UTF8-3 / UTF8-4 + # UTF8-1 = %x00-7F + # UTF8-2 = %xC2-DF UTF8-tail + # + # UTF8-3 = %xE0 %xA0-BF UTF8-tail / %xE1-EC 2( UTF8-tail ) / + # %xED %x80-9F UTF8-tail / %xEE-EF 2( UTF8-tail ) + # UTF8-4 = %xF0 %x90-BF 2( UTF8-tail ) / %xF1-F3 3( UTF8-tail ) / + # %xF4 %x80-8F 2( UTF8-tail ) + # UTF8-tail = %x80-BF + # --------------------------------------------------------------------- + #+ # @param string :str string to process. + # @return array containing codepoints (UTF-8 characters values) + # @access protected + # @author Nicola Asuni + # @since 1.53.0.TC005 (2005-01-05) + # + def UTF8StringToArray(str) + if (!@is_unicode) + return str; # string is not in unicode + end + + unicode = [] # array containing unicode values + bytes = [] # array containing single character byte sequences + numbytes = 1; # number of octetc needed to represent the UTF-8 character + + str = str.to_s; # force :str to be a string + + str.each_byte do |char| + if (bytes.length == 0) # get starting octect + if (char <= 0x7F) + unicode << char # use the character "as is" because is ASCII + numbytes = 1 + elsif ((char >> 0x05) == 0x06) # 2 bytes character (0x06 = 110 BIN) + bytes << ((char - 0xC0) << 0x06) + numbytes = 2 + elsif ((char >> 0x04) == 0x0E) # 3 bytes character (0x0E = 1110 BIN) + bytes << ((char - 0xE0) << 0x0C) + numbytes = 3 + elsif ((char >> 0x03) == 0x1E) # 4 bytes character (0x1E = 11110 BIN) + bytes << ((char - 0xF0) << 0x12) + numbytes = 4 + else + # use replacement character for other invalid sequences + unicode << 0xFFFD + bytes = [] + numbytes = 1 + end + elsif ((char >> 0x06) == 0x02) # bytes 2, 3 and 4 must start with 0x02 = 10 BIN + bytes << (char - 0x80) + if (bytes.length == numbytes) + # compose UTF-8 bytes to a single unicode value + char = bytes[0] + 1.upto(numbytes-1) do |j| + char += (bytes[j] << ((numbytes - j - 1) * 0x06)) + end + if (((char >= 0xD800) and (char <= 0xDFFF)) or (char >= 0x10FFFF)) + # The definition of UTF-8 prohibits encoding character numbers between + # U+D800 and U+DFFF, which are reserved for use with the UTF-16 + # encoding form (as surrogate pairs) and do not directly represent + # characters + unicode << 0xFFFD; # use replacement character + else + unicode << char; # add char to array + end + # reset data for next char + bytes = [] + numbytes = 1; + end + else + # use replacement character for other invalid sequences + unicode << 0xFFFD; + bytes = [] + numbytes = 1; + end + end + return unicode; + end + + # + # Converts UTF-8 strings to UTF16-BE.
+ # Encoding UTF-16: + # + # Encoding of a single character from an ISO 10646 character value to + # UTF-16 proceeds as follows. Let U be the character number, no greater + # than 0x10FFFF. + # + # 1) If U < 0x10000, encode U as a 16-bit unsigned integer and + # terminate. + # + # 2) Let U' = U - 0x10000. Because U is less than or equal to 0x10FFFF, + # U' must be less than or equal to 0xFFFFF. That is, U' can be + # represented in 20 bits. + # + # 3) Initialize two 16-bit unsigned integers, W1 and W2, to 0xD800 and + # 0xDC00, respectively. These integers each have 10 bits free to + # encode the character value, for a total of 20 bits. + # + # 4) Assign the 10 high-order bits of the 20-bit U' to the 10 low-order + # bits of W1 and the 10 low-order bits of U' to the 10 low-order + # bits of W2. Terminate. + # + # Graphically, steps 2 through 4 look like: + # U' = yyyyyyyyyyxxxxxxxxxx + # W1 = 110110yyyyyyyyyy + # W2 = 110111xxxxxxxxxx + #+ # @param string :str string to process. + # @param boolean :setbom if true set the Byte Order Mark (BOM = 0xFEFF) + # @return string + # @access protected + # @author Nicola Asuni + # @since 1.53.0.TC005 (2005-01-05) + # @uses UTF8StringToArray + # + def UTF8ToUTF16BE(str, setbom=true) + if (!@is_unicode) + return str; # string is not in unicode + end + outstr = ""; # string to be returned + unicode = UTF8StringToArray(str); # array containing UTF-8 unicode values + numitems = unicode.length; + + if (setbom) + outstr << "\xFE\xFF"; # Byte Order Mark (BOM) + end + unicode.each do |char| + if (char == 0xFFFD) + outstr << "\xFF\xFD"; # replacement character + elsif (char < 0x10000) + outstr << (char >> 0x08).chr; + outstr << (char & 0xFF).chr; + else + char -= 0x10000; + w1 = 0xD800 | (char >> 0x10); + w2 = 0xDC00 | (char & 0x3FF); + outstr << (w1 >> 0x08).chr; + outstr << (w1 & 0xFF).chr; + outstr << (w2 >> 0x08).chr; + outstr << (w2 & 0xFF).chr; + end + end + return outstr; + end + + # ==================================================== + + # + # Set header font. + # @param array :font font + # @since 1.1 + # + def SetHeaderFont(font) + @header_font = font; + end + alias_method :set_header_font, :SetHeaderFont + + # + # Set footer font. + # @param array :font font + # @since 1.1 + # + def SetFooterFont(font) + @footer_font = font; + end + alias_method :set_footer_font, :SetFooterFont + + # + # Set language array. + # @param array :language + # @since 1.1 + # + def SetLanguageArray(language) + @l = language; + end + alias_method :set_language_array, :SetLanguageArray + # + # Set document barcode. + # @param string :bc barcode + # + def SetBarcode(bc="") + @barcode = bc; + end + + # + # Print Barcode. + # @param int :x x position in user units + # @param int :y y position in user units + # @param int :w width in user units + # @param int :h height position in user units + # @param string :type type of barcode (I25, C128A, C128B, C128C, C39) + # @param string :style barcode style + # @param string :font font for text + # @param int :xres x resolution + # @param string :code code to print + # + def writeBarcode(x, y, w, h, type, style, font, xres, code) + require(File.dirname(__FILE__) + "/barcode/barcode.rb"); + require(File.dirname(__FILE__) + "/barcode/i25object.rb"); + require(File.dirname(__FILE__) + "/barcode/c39object.rb"); + require(File.dirname(__FILE__) + "/barcode/c128aobject.rb"); + require(File.dirname(__FILE__) + "/barcode/c128bobject.rb"); + require(File.dirname(__FILE__) + "/barcode/c128cobject.rb"); + + if (code.empty?) + return; + end + + if (style.empty?) + style = BCS_ALIGN_LEFT; + style |= BCS_IMAGE_PNG; + style |= BCS_TRANSPARENT; + #:style |= BCS_BORDER; + #:style |= BCS_DRAW_TEXT; + #:style |= BCS_STRETCH_TEXT; + #:style |= BCS_REVERSE_COLOR; + end + if (font.empty?) then font = BCD_DEFAULT_FONT; end + if (xres.empty?) then xres = BCD_DEFAULT_XRES; end + + scale_factor = 1.5 * xres * @k; + bc_w = (w * scale_factor).round #width in points + bc_h = (h * scale_factor).round #height in points + + case (type.upcase) + when "I25" + obj = I25Object.new(bc_w, bc_h, style, code); + when "C128A" + obj = C128AObject.new(bc_w, bc_h, style, code); + when "C128B" + obj = C128BObject.new(bc_w, bc_h, style, code); + when "C128C" + obj = C128CObject.new(bc_w, bc_h, style, code); + when "C39" + obj = C39Object.new(bc_w, bc_h, style, code); + end + + obj.SetFont(font); + obj.DrawObject(xres); + + #use a temporary file.... + tmpName = tempnam(@@k_path_cache,'img'); + imagepng(obj.getImage(), tmpName); + Image(tmpName, x, y, w, h, 'png'); + obj.DestroyObject(); + obj = nil + unlink(tmpName); + end + + # + # Returns the PDF data. + # + def GetPDFData() + if (@state < 3) + Close(); + end + return @buffer; + end + + # --- HTML PARSER FUNCTIONS --- + + # + # Allows to preserve some HTML formatting.
フォントスタイル | ||
---|---|---|
![]() | *太å—* | å¤ªå— |
![]() | _斜体_ | 斜体 |
![]() | +下線+ | 下線 |
![]() | -å–り消ã—ç·š- | |
??引用?? | 引用 | |
![]() | @コード@ | コード |
![]() | <pre> 複数行㮠コード </pre> |
++複数行㮠+コード ++ |
リスト | ||
![]() | * é …ç›®1 * é …ç›®2 |
|
![]() | # é …ç›®1 # é …ç›®2 |
|
見出㗠| ||
![]() | h1. タイトル1 | タイトル1 |
![]() | h2. タイトル2 | タイトル2 |
![]() | h3. タイトル3 | タイトル3 |
リンク | ||
http://foo.bar | http://foo.bar | |
"Foo":http://foo.bar | Foo | |
Redmine内ã®ãƒªãƒ³ã‚¯ | ||
![]() | [[Wiki page]] | Wiki page |
ãƒã‚±ãƒƒãƒˆ #12 | ãƒã‚±ãƒƒãƒˆ #12 | |
リビジョン r43 | リビジョン r43 | |
commit:f30e13e43 | f30e13e4 | |
source:some/file | source:some/file | |
ç”»åƒ | ||
![]() | !ç”»åƒURL! | |
!添付ファイルå! |
より詳細ãªãƒªãƒ•ァレンス
+ + + diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/00/006e32915b2ea0756e649e1fb61801b416e2d647.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/00/006e32915b2ea0756e649e1fb61801b416e2d647.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,88 @@ +# ActsAsWatchable +module Redmine + module Acts + module Watchable + def self.included(base) + base.extend ClassMethods + end + + module ClassMethods + def acts_as_watchable(options = {}) + return if self.included_modules.include?(Redmine::Acts::Watchable::InstanceMethods) + class_eval do + has_many :watchers, :as => :watchable, :dependent => :delete_all + has_many :watcher_users, :through => :watchers, :source => :user, :validate => false + + scope :watched_by, lambda { |user_id| + joins(:watchers). + where("#{Watcher.table_name}.user_id = ?", user_id) + } + attr_protected :watcher_ids, :watcher_user_ids + end + send :include, Redmine::Acts::Watchable::InstanceMethods + alias_method_chain :watcher_user_ids=, :uniq_ids + end + end + + module InstanceMethods + def self.included(base) + base.extend ClassMethods + end + + # Returns an array of users that are proposed as watchers + def addable_watcher_users + users = self.project.users.sort - self.watcher_users + if respond_to?(:visible?) + users.reject! {|user| !visible?(user)} + end + users + end + + # Adds user as a watcher + def add_watcher(user) + self.watchers << Watcher.new(:user => user) + end + + # Removes user from the watchers list + def remove_watcher(user) + return nil unless user && user.is_a?(User) + watchers.where(:user_id => user.id).delete_all + end + + # Adds/removes watcher + def set_watcher(user, watching=true) + watching ? add_watcher(user) : remove_watcher(user) + end + + # Overrides watcher_user_ids= to make user_ids uniq + def watcher_user_ids_with_uniq_ids=(user_ids) + if user_ids.is_a?(Array) + user_ids = user_ids.uniq + end + send :watcher_user_ids_without_uniq_ids=, user_ids + end + + # Returns true if object is watched by +user+ + def watched_by?(user) + !!(user && self.watcher_user_ids.detect {|uid| uid == user.id }) + end + + def notified_watchers + notified = watcher_users.active + notified.reject! {|user| user.mail.blank? || user.mail_notification == 'none'} + if respond_to?(:visible?) + notified.reject! {|user| !visible?(user)} + end + notified + end + + # Returns an array of watchers' email addresses + def watcher_recipients + notified_watchers.collect(&:mail) + end + + module ClassMethods; end + end + end + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/00/0081b5051789911922bdd0b18a28f0ad87c64e8d.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/00/0081b5051789911922bdd0b18a28f0ad87c64e8d.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,39 @@ +<%=l(:label_tracker)%> | ++ | <%=l(:button_sort)%> | ++ |
---|---|---|---|
<%= link_to h(tracker.name), edit_tracker_path(tracker) %> | ++ <% unless tracker.workflow_rules.count > 0 %> + + <%= l(:text_tracker_no_workflow) %> (<%= link_to l(:button_edit), workflows_edit_path(:tracker_id => tracker) %>) + + <% end %> + | ++ <%= reorder_links('tracker', {:action => 'update', :id => tracker}, :put) %> + | ++ <%= delete_link tracker_path(tracker) %> + | +
<%= pagination_links_full @tracker_pages %>
+ +<% html_title(l(:label_tracker_plural)) -%> diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/00/00bd76ee7194e922fe9340f2d39d910c9f79b20e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/00/00bd76ee7194e922fe9340f2d39d910c9f79b20e.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,162 @@ +module ObjectHelpers + def User.generate!(attributes={}) + @generated_user_login ||= 'user0' + @generated_user_login.succ! + user = User.new(attributes) + user.login = @generated_user_login.dup if user.login.blank? + user.mail = "#{@generated_user_login}@example.com" if user.mail.blank? + user.firstname = "Bob" if user.firstname.blank? + user.lastname = "Doe" if user.lastname.blank? + yield user if block_given? + user.save! + user + end + + def User.add_to_project(user, project, roles=nil) + roles = Role.find(1) if roles.nil? + roles = [roles] unless roles.is_a?(Array) + Member.create!(:principal => user, :project => project, :roles => roles) + end + + def Group.generate!(attributes={}) + @generated_group_name ||= 'Group 0' + @generated_group_name.succ! + group = Group.new(attributes) + group.name = @generated_group_name.dup if group.name.blank? + yield group if block_given? + group.save! + group + end + + def Project.generate!(attributes={}) + @generated_project_identifier ||= 'project-0000' + @generated_project_identifier.succ! + project = Project.new(attributes) + project.name = @generated_project_identifier.dup if project.name.blank? + project.identifier = @generated_project_identifier.dup if project.identifier.blank? + yield project if block_given? + project.save! + project + end + + def Project.generate_with_parent!(parent, attributes={}) + project = Project.generate!(attributes) + project.set_parent!(parent) + project + end + + def Tracker.generate!(attributes={}) + @generated_tracker_name ||= 'Tracker 0' + @generated_tracker_name.succ! + tracker = Tracker.new(attributes) + tracker.name = @generated_tracker_name.dup if tracker.name.blank? + yield tracker if block_given? + tracker.save! + tracker + end + + def Role.generate!(attributes={}) + @generated_role_name ||= 'Role 0' + @generated_role_name.succ! + role = Role.new(attributes) + role.name = @generated_role_name.dup if role.name.blank? + yield role if block_given? + role.save! + role + end + + def Issue.generate!(attributes={}) + issue = Issue.new(attributes) + issue.project ||= Project.find(1) + issue.tracker ||= issue.project.trackers.first + issue.subject = 'Generated' if issue.subject.blank? + issue.author ||= User.find(2) + yield issue if block_given? + issue.save! + issue + end + + # Generates an issue with 2 children and a grandchild + def Issue.generate_with_descendants!(attributes={}) + issue = Issue.generate!(attributes) + child = Issue.generate!(:project => issue.project, :subject => 'Child1', :parent_issue_id => issue.id) + Issue.generate!(:project => issue.project, :subject => 'Child2', :parent_issue_id => issue.id) + Issue.generate!(:project => issue.project, :subject => 'Child11', :parent_issue_id => child.id) + issue.reload + end + + def Journal.generate!(attributes={}) + journal = Journal.new(attributes) + journal.user ||= User.first + journal.journalized ||= Issue.first + yield journal if block_given? + journal.save! + journal + end + + def Version.generate!(attributes={}) + @generated_version_name ||= 'Version 0' + @generated_version_name.succ! + version = Version.new(attributes) + version.name = @generated_version_name.dup if version.name.blank? + yield version if block_given? + version.save! + version + end + + def TimeEntry.generate!(attributes={}) + entry = TimeEntry.new(attributes) + entry.user ||= User.find(2) + entry.issue ||= Issue.find(1) unless entry.project + entry.project ||= entry.issue.project + entry.activity ||= TimeEntryActivity.first + entry.spent_on ||= Date.today + entry.hours ||= 1.0 + entry.save! + entry + end + + def AuthSource.generate!(attributes={}) + @generated_auth_source_name ||= 'Auth 0' + @generated_auth_source_name.succ! + source = AuthSource.new(attributes) + source.name = @generated_auth_source_name.dup if source.name.blank? + yield source if block_given? + source.save! + source + end + + def Board.generate!(attributes={}) + @generated_board_name ||= 'Forum 0' + @generated_board_name.succ! + board = Board.new(attributes) + board.name = @generated_board_name.dup if board.name.blank? + board.description = @generated_board_name.dup if board.description.blank? + yield board if block_given? + board.save! + board + end + + def Attachment.generate!(attributes={}) + @generated_filename ||= 'testfile0' + @generated_filename.succ! + attributes = attributes.dup + attachment = Attachment.new(attributes) + attachment.container ||= Issue.find(1) + attachment.author ||= User.find(2) + attachment.filename = @generated_filename.dup if attachment.filename.blank? + attachment.save! + attachment + end + + def CustomField.generate!(attributes={}) + @generated_custom_field_name ||= 'Custom field 0' + @generated_custom_field_name.succ! + field = new(attributes) + field.name = @generated_custom_field_name.dup if field.name.blank? + field.field_format = 'string' if field.field_format.blank? + yield field if block_given? + field.save! + field + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/00/00c1a9b51dc321ecb980f25b62c13dfa7fd628ff.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/00/00c1a9b51dc321ecb980f25b62c13dfa7fd628ff.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,24 @@ +<%= error_messages_for 'auth_source' %> + +<%= f.text_field :name, :required => true %>
+<%= f.text_field :host, :required => true %>
+<%= f.text_field :port, :required => true, :size => 6 %> <%= f.check_box :tls, :no_label => true %> LDAPS
+<%= f.text_field :account %>
+<%= f.password_field :account_password, :label => :field_password, + :name => 'dummy_password', + :value => ((@auth_source.new_record? || @auth_source.account_password.blank?) ? '' : ('x'*15)), + :onfocus => "this.value=''; this.name='auth_source[account_password]';", + :onchange => "this.name='auth_source[account_password]';" %>
+<%= f.text_field :base_dn, :required => true, :size => 60 %>
+<%= f.text_field :filter, :size => 60, :label => :field_auth_source_ldap_filter %>
+<%= f.text_field :timeout, :size => 4 %>
+<%= f.check_box :onthefly_register, :label => :field_onthefly %>
++ | |||||||
---|---|---|---|---|---|---|---|
<%= avatar(user, :size => "14") %><%= link_to h(user.login), edit_user_path(user) %> | +<%= h(user.firstname) %> | +<%= h(user.lastname) %> | +<%= mail_to(h(user.mail)) %> | +<%= checked_image user.admin? %> | +<%= format_time(user.created_on) %> | +<%= format_time(user.last_login_on) unless user.last_login_on.nil? %> | ++ <%= change_status_link(user) %> + <%= delete_link user_path(user, :back_url => users_path(params)) unless User.current == user %> + | +
<%= pagination_links_full @user_pages, @user_count %>
+ +<% html_title(l(:label_user_plural)) -%> diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/01/0154cdd03545376764b58ded20a14138238e65d0.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/01/0154cdd03545376764b58ded20a14138238e65d0.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,178 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 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 File.expand_path('../../../../test_helper', __FILE__) + +class Redmine::Hook::ManagerTest < ActionView::TestCase + fixtures :projects, :users, :members, :member_roles, :roles, + :groups_users, + :trackers, :projects_trackers, + :enabled_modules, + :versions, + :issue_statuses, :issue_categories, :issue_relations, + :enumerations, + :issues + + # Some hooks that are manually registered in these tests + class TestHook < Redmine::Hook::ViewListener; end + + class TestHook1 < TestHook + def view_layouts_base_html_head(context) + 'Test hook 1 listener.' + end + end + + class TestHook2 < TestHook + def view_layouts_base_html_head(context) + 'Test hook 2 listener.' + end + end + + class TestHook3 < TestHook + def view_layouts_base_html_head(context) + "Context keys: #{context.keys.collect(&:to_s).sort.join(', ')}." + end + end + + class TestLinkToHook < TestHook + def view_layouts_base_html_head(context) + link_to('Issues', :controller => 'issues') + end + end + + class TestHookHelperController < ActionController::Base + include Redmine::Hook::Helper + end + + class TestHookHelperView < ActionView::Base + include Redmine::Hook::Helper + end + + Redmine::Hook.clear_listeners + + def setup + @hook_module = Redmine::Hook + end + + def teardown + @hook_module.clear_listeners + end + + def test_clear_listeners + assert_equal 0, @hook_module.hook_listeners(:view_layouts_base_html_head).size + @hook_module.add_listener(TestHook1) + @hook_module.add_listener(TestHook2) + assert_equal 2, @hook_module.hook_listeners(:view_layouts_base_html_head).size + + @hook_module.clear_listeners + assert_equal 0, @hook_module.hook_listeners(:view_layouts_base_html_head).size + end + + def test_add_listener + assert_equal 0, @hook_module.hook_listeners(:view_layouts_base_html_head).size + @hook_module.add_listener(TestHook1) + assert_equal 1, @hook_module.hook_listeners(:view_layouts_base_html_head).size + end + + def test_call_hook + @hook_module.add_listener(TestHook1) + assert_equal ['Test hook 1 listener.'], hook_helper.call_hook(:view_layouts_base_html_head) + end + + def test_call_hook_with_context + @hook_module.add_listener(TestHook3) + assert_equal ['Context keys: bar, controller, foo, hook_caller, project, request.'], + hook_helper.call_hook(:view_layouts_base_html_head, :foo => 1, :bar => 'a') + end + + def test_call_hook_with_multiple_listeners + @hook_module.add_listener(TestHook1) + @hook_module.add_listener(TestHook2) + assert_equal ['Test hook 1 listener.', 'Test hook 2 listener.'], hook_helper.call_hook(:view_layouts_base_html_head) + end + + # Context: Redmine::Hook::Helper.call_hook default_url + def test_call_hook_default_url_options + @hook_module.add_listener(TestLinkToHook) + + assert_equal ['Issues'], hook_helper.call_hook(:view_layouts_base_html_head) + end + + # Context: Redmine::Hook::Helper.call_hook + def test_call_hook_with_project_added_to_context + @hook_module.add_listener(TestHook3) + assert_match /project/i, hook_helper.call_hook(:view_layouts_base_html_head)[0] + end + + def test_call_hook_from_controller_with_controller_added_to_context + @hook_module.add_listener(TestHook3) + assert_match /controller/i, hook_helper.call_hook(:view_layouts_base_html_head)[0] + end + + def test_call_hook_from_controller_with_request_added_to_context + @hook_module.add_listener(TestHook3) + assert_match /request/i, hook_helper.call_hook(:view_layouts_base_html_head)[0] + end + + def test_call_hook_from_view_with_project_added_to_context + @hook_module.add_listener(TestHook3) + assert_match /project/i, view_hook_helper.call_hook(:view_layouts_base_html_head) + end + + def test_call_hook_from_view_with_controller_added_to_context + @hook_module.add_listener(TestHook3) + assert_match /controller/i, view_hook_helper.call_hook(:view_layouts_base_html_head) + end + + def test_call_hook_from_view_with_request_added_to_context + @hook_module.add_listener(TestHook3) + assert_match /request/i, view_hook_helper.call_hook(:view_layouts_base_html_head) + end + + def test_call_hook_from_view_should_join_responses_with_a_space + @hook_module.add_listener(TestHook1) + @hook_module.add_listener(TestHook2) + assert_equal 'Test hook 1 listener. Test hook 2 listener.', + view_hook_helper.call_hook(:view_layouts_base_html_head) + end + + def test_call_hook_should_not_change_the_default_url_for_email_notifications + issue = Issue.find(1) + + ActionMailer::Base.deliveries.clear + Mailer.issue_add(issue).deliver + mail = ActionMailer::Base.deliveries.last + + @hook_module.add_listener(TestLinkToHook) + hook_helper.call_hook(:view_layouts_base_html_head) + + ActionMailer::Base.deliveries.clear + Mailer.issue_add(issue).deliver + mail2 = ActionMailer::Base.deliveries.last + + assert_equal mail_body(mail), mail_body(mail2) + end + + def hook_helper + @hook_helper ||= TestHookHelperController.new + end + + def view_hook_helper + @view_hook_helper ||= TestHookHelperView.new(Rails.root.to_s + '/app/views') + end +end + diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/01/019777b51c435b18c756e5573fde25c903195b68.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/01/019777b51c435b18c756e5573fde25c903195b68.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,37 @@ +require 'rexml/document' + +module Redmine + module VERSION #:nodoc: + MAJOR = 2 + MINOR = 4 + TINY = 2 + + # Branch values: + # * official release: nil + # * stable branch: stable + # * trunk: devel + BRANCH = 'stable' + + # Retrieves the revision from the working copy + def self.revision + if File.directory?(File.join(Rails.root, '.svn')) + begin + path = Redmine::Scm::Adapters::AbstractAdapter.shell_quote(Rails.root.to_s) + if `svn info --xml #{path}` =~ /revision="(\d+)"/ + return $1.to_i + end + rescue + # Could not find the current revision + end + end + nil + end + + REVISION = self.revision + ARRAY = [MAJOR, MINOR, TINY, BRANCH, REVISION].compact + STRING = ARRAY.join('.') + + def self.to_a; ARRAY end + def self.to_s; STRING end + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/01/01b21d04c9e912327dc0059ef282ceb1a603f54f.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/01/01b21d04c9e912327dc0059ef282ceb1a603f54f.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,90 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 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 File.expand_path('../../test_helper', __FILE__) + +class AutoCompletesControllerTest < ActionController::TestCase + fixtures :projects, :issues, :issue_statuses, + :enumerations, :users, :issue_categories, + :trackers, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules, + :journals, :journal_details + + def test_issues_should_not_be_case_sensitive + get :issues, :project_id => 'ecookbook', :q => 'ReCiPe' + assert_response :success + assert_not_nil assigns(:issues) + assert assigns(:issues).detect {|issue| issue.subject.match /recipe/} + end + + def test_issues_should_accept_term_param + get :issues, :project_id => 'ecookbook', :term => 'ReCiPe' + assert_response :success + assert_not_nil assigns(:issues) + assert assigns(:issues).detect {|issue| issue.subject.match /recipe/} + end + + def test_issues_should_return_issue_with_given_id + get :issues, :project_id => 'subproject1', :q => '13' + assert_response :success + assert_not_nil assigns(:issues) + assert assigns(:issues).include?(Issue.find(13)) + end + + def test_issues_should_return_issue_with_given_id_preceded_with_hash + get :issues, :project_id => 'subproject1', :q => '#13' + assert_response :success + assert_not_nil assigns(:issues) + assert assigns(:issues).include?(Issue.find(13)) + end + + def test_auto_complete_with_scope_all_should_search_other_projects + get :issues, :project_id => 'ecookbook', :q => '13', :scope => 'all' + assert_response :success + assert_not_nil assigns(:issues) + assert assigns(:issues).include?(Issue.find(13)) + end + + def test_auto_complete_without_project_should_search_all_projects + get :issues, :q => '13' + assert_response :success + assert_not_nil assigns(:issues) + assert assigns(:issues).include?(Issue.find(13)) + end + + def test_auto_complete_without_scope_all_should_not_search_other_projects + get :issues, :project_id => 'ecookbook', :q => '13' + assert_response :success + assert_equal [], assigns(:issues) + end + + def test_issues_should_return_json + get :issues, :project_id => 'subproject1', :q => '13' + assert_response :success + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Array, json + issue = json.first + assert_kind_of Hash, issue + assert_equal 13, issue['id'] + assert_equal 13, issue['value'] + assert_equal 'Bug #13: Subproject issue two', issue['label'] + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/01/01cfbfdc38592dff3cbe5b1b36118aa0e1e65223.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/01/01cfbfdc38592dff3cbe5b1b36118aa0e1e65223.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,122 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 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 File.expand_path('../../test_helper', __FILE__) + +class GanttsControllerTest < ActionController::TestCase + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules, + :versions + + def test_gantt_should_work + i2 = Issue.find(2) + i2.update_attribute(:due_date, 1.month.from_now) + get :show, :project_id => 1 + assert_response :success + assert_template 'gantts/show' + assert_not_nil assigns(:gantt) + # Issue with start and due dates + i = Issue.find(1) + assert_not_nil i.due_date + assert_select "div a.issue", /##{i.id}/ + # Issue with on a targeted version should not be in the events but loaded in the html + i = Issue.find(2) + assert_select "div a.issue", /##{i.id}/ + end + + def test_gantt_should_work_without_issue_due_dates + Issue.update_all("due_date = NULL") + get :show, :project_id => 1 + assert_response :success + assert_template 'gantts/show' + assert_not_nil assigns(:gantt) + end + + def test_gantt_should_work_without_issue_and_version_due_dates + Issue.update_all("due_date = NULL") + Version.update_all("effective_date = NULL") + get :show, :project_id => 1 + assert_response :success + assert_template 'gantts/show' + assert_not_nil assigns(:gantt) + end + + def test_gantt_should_work_cross_project + get :show + assert_response :success + assert_template 'gantts/show' + assert_not_nil assigns(:gantt) + assert_not_nil assigns(:gantt).query + assert_nil assigns(:gantt).project + end + + def test_gantt_should_not_disclose_private_projects + get :show + assert_response :success + assert_template 'gantts/show' + assert_tag 'a', :content => /eCookbook/ + # Root private project + assert_no_tag 'a', {:content => /OnlineStore/} + # Private children of a public project + assert_no_tag 'a', :content => /Private child of eCookbook/ + end + + def test_gantt_should_display_relations + IssueRelation.delete_all + issue1 = Issue.generate!(:start_date => 1.day.from_now.to_date, :due_date => 3.day.from_now.to_date) + issue2 = Issue.generate!(:start_date => 1.day.from_now.to_date, :due_date => 3.day.from_now.to_date) + IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => 'precedes') + + get :show + assert_response :success + + relations = assigns(:gantt).relations + assert_kind_of Hash, relations + assert relations.present? + assert_select 'div.task_todo[id=?][data-rels*=?]', "task-todo-issue-#{issue1.id}", issue2.id.to_s + assert_select 'div.task_todo[id=?]:not([data-rels])', "task-todo-issue-#{issue2.id}" + end + + def test_gantt_should_export_to_pdf + get :show, :project_id => 1, :format => 'pdf' + assert_response :success + assert_equal 'application/pdf', @response.content_type + assert @response.body.starts_with?('%PDF') + assert_not_nil assigns(:gantt) + end + + def test_gantt_should_export_to_pdf_cross_project + get :show, :format => 'pdf' + assert_response :success + assert_equal 'application/pdf', @response.content_type + assert @response.body.starts_with?('%PDF') + assert_not_nil assigns(:gantt) + end + + if Object.const_defined?(:Magick) + def test_gantt_should_export_to_png + get :show, :project_id => 1, :format => 'png' + assert_response :success + assert_equal 'image/png', @response.content_type + end + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/02/0235738b45d2757cc6462ce5ba2c1e87548fc84e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/02/0235738b45d2757cc6462ce5ba2c1e87548fc84e.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,13 @@ +class CreateQueriesRoles < ActiveRecord::Migration + def self.up + create_table :queries_roles, :id => false do |t| + t.column :query_id, :integer, :null => false + t.column :role_id, :integer, :null => false + end + add_index :queries_roles, [:query_id, :role_id], :unique => true, :name => :queries_roles_ids + end + + def self.down + drop_table :queries_roles + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/02/026bb2d46eaa121e79862de3515bb99ff0827ca3.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/02/026bb2d46eaa121e79862de3515bb99ff0827ca3.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,943 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 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. + +module Redmine + module Helpers + # Simple class to handle gantt chart data + class Gantt + include ERB::Util + include Redmine::I18n + include Redmine::Utils::DateCalculation + + # Relation types that are rendered + DRAW_TYPES = { + IssueRelation::TYPE_BLOCKS => { :landscape_margin => 16, :color => '#F34F4F' }, + IssueRelation::TYPE_PRECEDES => { :landscape_margin => 20, :color => '#628FEA' } + }.freeze + + # :nodoc: + # Some utility methods for the PDF export + class PDF + MaxCharactorsForSubject = 45 + TotalWidth = 280 + LeftPaneWidth = 100 + + def self.right_pane_width + TotalWidth - LeftPaneWidth + end + end + + attr_reader :year_from, :month_from, :date_from, :date_to, :zoom, :months, :truncated, :max_rows + attr_accessor :query + attr_accessor :project + attr_accessor :view + + def initialize(options={}) + options = options.dup + if options[:year] && options[:year].to_i >0 + @year_from = options[:year].to_i + if options[:month] && options[:month].to_i >=1 && options[:month].to_i <= 12 + @month_from = options[:month].to_i + else + @month_from = 1 + end + else + @month_from ||= Date.today.month + @year_from ||= Date.today.year + end + zoom = (options[:zoom] || User.current.pref[:gantt_zoom]).to_i + @zoom = (zoom > 0 && zoom < 5) ? zoom : 2 + months = (options[:months] || User.current.pref[:gantt_months]).to_i + @months = (months > 0 && months < 25) ? months : 6 + # Save gantt parameters as user preference (zoom and months count) + if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] || + @months != User.current.pref[:gantt_months])) + User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months + User.current.preference.save + end + @date_from = Date.civil(@year_from, @month_from, 1) + @date_to = (@date_from >> @months) - 1 + @subjects = '' + @lines = '' + @number_of_rows = nil + @issue_ancestors = [] + @truncated = false + if options.has_key?(:max_rows) + @max_rows = options[:max_rows] + else + @max_rows = Setting.gantt_items_limit.blank? ? nil : Setting.gantt_items_limit.to_i + end + end + + def common_params + { :controller => 'gantts', :action => 'show', :project_id => @project } + end + + def params + common_params.merge({:zoom => zoom, :year => year_from, + :month => month_from, :months => months}) + end + + def params_previous + common_params.merge({:year => (date_from << months).year, + :month => (date_from << months).month, + :zoom => zoom, :months => months}) + end + + def params_next + common_params.merge({:year => (date_from >> months).year, + :month => (date_from >> months).month, + :zoom => zoom, :months => months}) + end + + # Returns the number of rows that will be rendered on the Gantt chart + def number_of_rows + return @number_of_rows if @number_of_rows + rows = projects.inject(0) {|total, p| total += number_of_rows_on_project(p)} + rows > @max_rows ? @max_rows : rows + end + + # Returns the number of rows that will be used to list a project on + # the Gantt chart. This will recurse for each subproject. + def number_of_rows_on_project(project) + return 0 unless projects.include?(project) + count = 1 + count += project_issues(project).size + count += project_versions(project).size + count + end + + # Renders the subjects of the Gantt chart, the left side. + def subjects(options={}) + render(options.merge(:only => :subjects)) unless @subjects_rendered + @subjects + end + + # Renders the lines of the Gantt chart, the right side + def lines(options={}) + render(options.merge(:only => :lines)) unless @lines_rendered + @lines + end + + # Returns issues that will be rendered + def issues + @issues ||= @query.issues( + :include => [:assigned_to, :tracker, :priority, :category, :fixed_version], + :order => "#{Project.table_name}.lft ASC, #{Issue.table_name}.id ASC", + :limit => @max_rows + ) + end + + # Returns a hash of the relations between the issues that are present on the gantt + # and that should be displayed, grouped by issue ids. + def relations + return @relations if @relations + if issues.any? + issue_ids = issues.map(&:id) + @relations = IssueRelation. + where(:issue_from_id => issue_ids, :issue_to_id => issue_ids, :relation_type => DRAW_TYPES.keys). + group_by(&:issue_from_id) + else + @relations = {} + end + end + + # Return all the project nodes that will be displayed + def projects + return @projects if @projects + ids = issues.collect(&:project).uniq.collect(&:id) + if ids.any? + # All issues projects and their visible ancestors + @projects = Project.visible.all( + :joins => "LEFT JOIN #{Project.table_name} child ON #{Project.table_name}.lft <= child.lft AND #{Project.table_name}.rgt >= child.rgt", + :conditions => ["child.id IN (?)", ids], + :order => "#{Project.table_name}.lft ASC" + ).uniq + else + @projects = [] + end + end + + # Returns the issues that belong to +project+ + def project_issues(project) + @issues_by_project ||= issues.group_by(&:project) + @issues_by_project[project] || [] + end + + # Returns the distinct versions of the issues that belong to +project+ + def project_versions(project) + project_issues(project).collect(&:fixed_version).compact.uniq + end + + # Returns the issues that belong to +project+ and are assigned to +version+ + def version_issues(project, version) + project_issues(project).select {|issue| issue.fixed_version == version} + end + + def render(options={}) + options = {:top => 0, :top_increment => 20, + :indent_increment => 20, :render => :subject, + :format => :html}.merge(options) + indent = options[:indent] || 4 + @subjects = '' unless options[:only] == :lines + @lines = '' unless options[:only] == :subjects + @number_of_rows = 0 + Project.project_tree(projects) do |project, level| + options[:indent] = indent + level * options[:indent_increment] + render_project(project, options) + break if abort? + end + @subjects_rendered = true unless options[:only] == :lines + @lines_rendered = true unless options[:only] == :subjects + render_end(options) + end + + def render_project(project, options={}) + subject_for_project(project, options) unless options[:only] == :lines + line_for_project(project, options) unless options[:only] == :subjects + options[:top] += options[:top_increment] + options[:indent] += options[:indent_increment] + @number_of_rows += 1 + return if abort? + issues = project_issues(project).select {|i| i.fixed_version.nil?} + sort_issues!(issues) + if issues + render_issues(issues, options) + return if abort? + end + versions = project_versions(project) + versions.each do |version| + render_version(project, version, options) + end + # Remove indent to hit the next sibling + options[:indent] -= options[:indent_increment] + end + + def render_issues(issues, options={}) + @issue_ancestors = [] + issues.each do |i| + subject_for_issue(i, options) unless options[:only] == :lines + line_for_issue(i, options) unless options[:only] == :subjects + options[:top] += options[:top_increment] + @number_of_rows += 1 + break if abort? + end + options[:indent] -= (options[:indent_increment] * @issue_ancestors.size) + end + + def render_version(project, version, options={}) + # Version header + subject_for_version(version, options) unless options[:only] == :lines + line_for_version(version, options) unless options[:only] == :subjects + options[:top] += options[:top_increment] + @number_of_rows += 1 + return if abort? + issues = version_issues(project, version) + if issues + sort_issues!(issues) + # Indent issues + options[:indent] += options[:indent_increment] + render_issues(issues, options) + options[:indent] -= options[:indent_increment] + end + end + + def render_end(options={}) + case options[:format] + when :pdf + options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top]) + end + end + + def subject_for_project(project, options) + case options[:format] + when :html + html_class = "" + html_class << 'icon icon-projects ' + html_class << (project.overdue? ? 'project-overdue' : '') + s = view.link_to_project(project).html_safe + subject = view.content_tag(:span, s, + :class => html_class).html_safe + html_subject(options, subject, :css => "project-name") + when :image + image_subject(options, project.name) + when :pdf + pdf_new_page?(options) + pdf_subject(options, project.name) + end + end + + def line_for_project(project, options) + # Skip versions that don't have a start_date or due date + if project.is_a?(Project) && project.start_date && project.due_date + options[:zoom] ||= 1 + options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom] + coords = coordinates(project.start_date, project.due_date, nil, options[:zoom]) + label = h(project) + case options[:format] + when :html + html_task(options, coords, :css => "project task", :label => label, :markers => true) + when :image + image_task(options, coords, :label => label, :markers => true, :height => 3) + when :pdf + pdf_task(options, coords, :label => label, :markers => true, :height => 0.8) + end + else + '' + end + end + + def subject_for_version(version, options) + case options[:format] + when :html + html_class = "" + html_class << 'icon icon-package ' + html_class << (version.behind_schedule? ? 'version-behind-schedule' : '') << " " + html_class << (version.overdue? ? 'version-overdue' : '') + html_class << ' version-closed' unless version.open? + if version.start_date && version.due_date && version.completed_pourcent + progress_date = calc_progress_date(version.start_date, + version.due_date, version.completed_pourcent) + html_class << ' behind-start-date' if progress_date < self.date_from + html_class << ' over-end-date' if progress_date > self.date_to + end + s = view.link_to_version(version).html_safe + subject = view.content_tag(:span, s, + :class => html_class).html_safe + html_subject(options, subject, :css => "version-name", + :id => "version-#{version.id}") + when :image + image_subject(options, version.to_s_with_project) + when :pdf + pdf_new_page?(options) + pdf_subject(options, version.to_s_with_project) + end + end + + def line_for_version(version, options) + # Skip versions that don't have a start_date + if version.is_a?(Version) && version.due_date && version.start_date + options[:zoom] ||= 1 + options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom] + coords = coordinates(version.start_date, + version.due_date, version.completed_percent, + options[:zoom]) + label = "#{h version} #{h version.completed_percent.to_i.to_s}%" + label = h("#{version.project} -") + label unless @project && @project == version.project + case options[:format] + when :html + html_task(options, coords, :css => "version task", + :label => label, :markers => true, :version => version) + when :image + image_task(options, coords, :label => label, :markers => true, :height => 3) + when :pdf + pdf_task(options, coords, :label => label, :markers => true, :height => 0.8) + end + else + '' + end + end + + def subject_for_issue(issue, options) + while @issue_ancestors.any? && !issue.is_descendant_of?(@issue_ancestors.last) + @issue_ancestors.pop + options[:indent] -= options[:indent_increment] + end + output = case options[:format] + when :html + css_classes = '' + css_classes << ' issue-overdue' if issue.overdue? + css_classes << ' issue-behind-schedule' if issue.behind_schedule? + css_classes << ' icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to + css_classes << ' issue-closed' if issue.closed? + if issue.start_date && issue.due_before && issue.done_ratio + progress_date = calc_progress_date(issue.start_date, + issue.due_before, issue.done_ratio) + css_classes << ' behind-start-date' if progress_date < self.date_from + css_classes << ' over-end-date' if progress_date > self.date_to + end + s = "".html_safe + if issue.assigned_to.present? + assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name + s << view.avatar(issue.assigned_to, + :class => 'gravatar icon-gravatar', + :size => 10, + :title => assigned_string).to_s.html_safe + end + s << view.link_to_issue(issue).html_safe + subject = view.content_tag(:span, s, :class => css_classes).html_safe + html_subject(options, subject, :css => "issue-subject", + :title => issue.subject, :id => "issue-#{issue.id}") + "\n" + when :image + image_subject(options, issue.subject) + when :pdf + pdf_new_page?(options) + pdf_subject(options, issue.subject) + end + unless issue.leaf? + @issue_ancestors << issue + options[:indent] += options[:indent_increment] + end + output + end + + def line_for_issue(issue, options) + # Skip issues that don't have a due_before (due_date or version's due_date) + if issue.is_a?(Issue) && issue.due_before + coords = coordinates(issue.start_date, issue.due_before, issue.done_ratio, options[:zoom]) + label = "#{issue.status.name} #{issue.done_ratio}%" + case options[:format] + when :html + html_task(options, coords, + :css => "task " + (issue.leaf? ? 'leaf' : 'parent'), + :label => label, :issue => issue, + :markers => !issue.leaf?) + when :image + image_task(options, coords, :label => label) + when :pdf + pdf_task(options, coords, :label => label) + end + else + '' + end + end + + # Generates a gantt image + # Only defined if RMagick is avalaible + def to_image(format='PNG') + date_to = (@date_from >> @months) - 1 + show_weeks = @zoom > 1 + show_days = @zoom > 2 + subject_width = 400 + header_height = 18 + # width of one day in pixels + zoom = @zoom * 2 + g_width = (@date_to - @date_from + 1) * zoom + g_height = 20 * number_of_rows + 30 + headers_height = (show_weeks ? 2 * header_height : header_height) + height = g_height + headers_height + imgl = Magick::ImageList.new + imgl.new_image(subject_width + g_width + 1, height) + gc = Magick::Draw.new + gc.font = Redmine::Configuration['rmagick_font_path'] || "" + # Subjects + gc.stroke('transparent') + subjects(:image => gc, :top => (headers_height + 20), :indent => 4, :format => :image) + # Months headers + month_f = @date_from + left = subject_width + @months.times do + width = ((month_f >> 1) - month_f) * zoom + gc.fill('white') + gc.stroke('grey') + gc.stroke_width(1) + gc.rectangle(left, 0, left + width, height) + gc.fill('black') + gc.stroke('transparent') + gc.stroke_width(1) + gc.text(left.round + 8, 14, "#{month_f.year}-#{month_f.month}") + left = left + width + month_f = month_f >> 1 + end + # Weeks headers + if show_weeks + left = subject_width + height = header_height + if @date_from.cwday == 1 + # date_from is monday + week_f = date_from + else + # find next monday after date_from + week_f = @date_from + (7 - @date_from.cwday + 1) + width = (7 - @date_from.cwday + 1) * zoom + gc.fill('white') + gc.stroke('grey') + gc.stroke_width(1) + gc.rectangle(left, header_height, left + width, 2 * header_height + g_height - 1) + left = left + width + end + while week_f <= date_to + width = (week_f + 6 <= date_to) ? 7 * zoom : (date_to - week_f + 1) * zoom + gc.fill('white') + gc.stroke('grey') + gc.stroke_width(1) + gc.rectangle(left.round, header_height, left.round + width, 2 * header_height + g_height - 1) + gc.fill('black') + gc.stroke('transparent') + gc.stroke_width(1) + gc.text(left.round + 2, header_height + 14, week_f.cweek.to_s) + left = left + width + week_f = week_f + 7 + end + end + # Days details (week-end in grey) + if show_days + left = subject_width + height = g_height + header_height - 1 + wday = @date_from.cwday + (date_to - @date_from + 1).to_i.times do + width = zoom + gc.fill(non_working_week_days.include?(wday) ? '#eee' : 'white') + gc.stroke('#ddd') + gc.stroke_width(1) + gc.rectangle(left, 2 * header_height, left + width, 2 * header_height + g_height - 1) + left = left + width + wday = wday + 1 + wday = 1 if wday > 7 + end + end + # border + gc.fill('transparent') + gc.stroke('grey') + gc.stroke_width(1) + gc.rectangle(0, 0, subject_width + g_width, headers_height) + gc.stroke('black') + gc.rectangle(0, 0, subject_width + g_width, g_height + headers_height - 1) + # content + top = headers_height + 20 + gc.stroke('transparent') + lines(:image => gc, :top => top, :zoom => zoom, + :subject_width => subject_width, :format => :image) + # today red line + if Date.today >= @date_from and Date.today <= date_to + gc.stroke('red') + x = (Date.today - @date_from + 1) * zoom + subject_width + gc.line(x, headers_height, x, headers_height + g_height - 1) + end + gc.draw(imgl) + imgl.format = format + imgl.to_blob + end if Object.const_defined?(:Magick) + + def to_pdf + pdf = ::Redmine::Export::PDF::ITCPDF.new(current_language) + pdf.SetTitle("#{l(:label_gantt)} #{project}") + pdf.alias_nb_pages + pdf.footer_date = format_date(Date.today) + pdf.AddPage("L") + pdf.SetFontStyle('B', 12) + pdf.SetX(15) + pdf.RDMCell(PDF::LeftPaneWidth, 20, project.to_s) + pdf.Ln + pdf.SetFontStyle('B', 9) + subject_width = PDF::LeftPaneWidth + header_height = 5 + headers_height = header_height + show_weeks = false + show_days = false + if self.months < 7 + show_weeks = true + headers_height = 2 * header_height + if self.months < 3 + show_days = true + headers_height = 3 * header_height + end + end + g_width = PDF.right_pane_width + zoom = (g_width) / (self.date_to - self.date_from + 1) + g_height = 120 + t_height = g_height + headers_height + y_start = pdf.GetY + # Months headers + month_f = self.date_from + left = subject_width + height = header_height + self.months.times do + width = ((month_f >> 1) - month_f) * zoom + pdf.SetY(y_start) + pdf.SetX(left) + pdf.RDMCell(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_height + if self.date_from.cwday == 1 + # self.date_from is monday + week_f = self.date_from + else + # find next monday after self.date_from + week_f = self.date_from + (7 - self.date_from.cwday + 1) + width = (7 - self.date_from.cwday + 1) * zoom-1 + pdf.SetY(y_start + header_height) + pdf.SetX(left) + pdf.RDMCell(width + 1, height, "", "LTR") + left = left + width + 1 + end + while week_f <= self.date_to + width = (week_f + 6 <= self.date_to) ? 7 * zoom : (self.date_to - week_f + 1) * zoom + pdf.SetY(y_start + header_height) + pdf.SetX(left) + pdf.RDMCell(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_height + wday = self.date_from.cwday + pdf.SetFontStyle('B', 7) + (self.date_to - self.date_from + 1).to_i.times do + width = zoom + pdf.SetY(y_start + 2 * header_height) + pdf.SetX(left) + pdf.RDMCell(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.RDMCell(subject_width + g_width - 15, headers_height, "", 1) + # Tasks + top = headers_height + y_start + options = { + :top => top, + :zoom => zoom, + :subject_width => subject_width, + :g_width => g_width, + :indent => 0, + :indent_increment => 5, + :top_increment => 5, + :format => :pdf, + :pdf => pdf + } + render(options) + pdf.Output + end + + private + + def coordinates(start_date, end_date, progress, zoom=nil) + zoom ||= @zoom + coords = {} + if start_date && end_date && start_date < self.date_to && end_date > self.date_from + if start_date > self.date_from + coords[:start] = start_date - self.date_from + coords[:bar_start] = start_date - self.date_from + else + coords[:bar_start] = 0 + end + if end_date < self.date_to + coords[:end] = end_date - self.date_from + coords[:bar_end] = end_date - self.date_from + 1 + else + coords[:bar_end] = self.date_to - self.date_from + 1 + end + if progress + progress_date = calc_progress_date(start_date, end_date, progress) + if progress_date > self.date_from && progress_date > start_date + if progress_date < self.date_to + coords[:bar_progress_end] = progress_date - self.date_from + else + coords[:bar_progress_end] = self.date_to - self.date_from + 1 + end + end + if progress_date < Date.today + late_date = [Date.today, end_date].min + if late_date > self.date_from && late_date > start_date + if late_date < self.date_to + coords[:bar_late_end] = late_date - self.date_from + 1 + else + coords[:bar_late_end] = self.date_to - self.date_from + 1 + end + end + end + end + end + # Transforms dates into pixels witdh + coords.keys.each do |key| + coords[key] = (coords[key] * zoom).floor + end + coords + end + + def calc_progress_date(start_date, end_date, progress) + start_date + (end_date - start_date + 1) * (progress / 100.0) + end + + # TODO: Sorts a collection of issues by start_date, due_date, id for gantt rendering + def sort_issues!(issues) + issues.sort! { |a, b| gantt_issue_compare(a, b) } + end + + # TODO: top level issues should be sorted by start date + def gantt_issue_compare(x, y) + if x.root_id == y.root_id + x.lft <=> y.lft + else + x.root_id <=> y.root_id + end + end + + def current_limit + if @max_rows + @max_rows - @number_of_rows + else + nil + end + end + + def abort? + if @max_rows && @number_of_rows >= @max_rows + @truncated = true + end + end + + def pdf_new_page?(options) + if options[:top] > 180 + options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top]) + options[:pdf].AddPage("L") + options[:top] = 15 + options[:pdf].Line(15, options[:top] - 0.1, PDF::TotalWidth, options[:top] - 0.1) + end + end + + def html_subject(params, subject, options={}) + style = "position: absolute;top:#{params[:top]}px;left:#{params[:indent]}px;" + style << "width:#{params[:subject_width] - params[:indent]}px;" if params[:subject_width] + output = view.content_tag(:div, subject, + :class => options[:css], :style => style, + :title => options[:title], + :id => options[:id]) + @subjects << output + output + end + + def pdf_subject(params, subject, options={}) + params[:pdf].SetY(params[:top]) + params[:pdf].SetX(15) + char_limit = PDF::MaxCharactorsForSubject - params[:indent] + params[:pdf].RDMCell(params[:subject_width] - 15, 5, + (" " * params[:indent]) + + subject.to_s.sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), + "LR") + params[:pdf].SetY(params[:top]) + params[:pdf].SetX(params[:subject_width]) + params[:pdf].RDMCell(params[:g_width], 5, "", "LR") + end + + def image_subject(params, subject, options={}) + params[:image].fill('black') + params[:image].stroke('transparent') + params[:image].stroke_width(1) + params[:image].text(params[:indent], params[:top] + 2, subject) + end + + def issue_relations(issue) + rels = {} + if relations[issue.id] + relations[issue.id].each do |relation| + (rels[relation.relation_type] ||= []) << relation.issue_to_id + end + end + rels + end + + def html_task(params, coords, options={}) + output = '' + # Renders the task bar, with progress and late + if coords[:bar_start] && coords[:bar_end] + width = coords[:bar_end] - coords[:bar_start] - 2 + style = "" + style << "top:#{params[:top]}px;" + style << "left:#{coords[:bar_start]}px;" + style << "width:#{width}px;" + html_id = "task-todo-issue-#{options[:issue].id}" if options[:issue] + html_id = "task-todo-version-#{options[:version].id}" if options[:version] + content_opt = {:style => style, + :class => "#{options[:css]} task_todo", + :id => html_id} + if options[:issue] + rels = issue_relations(options[:issue]) + if rels.present? + content_opt[:data] = {"rels" => rels.to_json} + end + end + output << view.content_tag(:div, ' '.html_safe, content_opt) + if coords[:bar_late_end] + width = coords[:bar_late_end] - coords[:bar_start] - 2 + style = "" + style << "top:#{params[:top]}px;" + style << "left:#{coords[:bar_start]}px;" + style << "width:#{width}px;" + output << view.content_tag(:div, ' '.html_safe, + :style => style, + :class => "#{options[:css]} task_late") + end + if coords[:bar_progress_end] + width = coords[:bar_progress_end] - coords[:bar_start] - 2 + style = "" + style << "top:#{params[:top]}px;" + style << "left:#{coords[:bar_start]}px;" + style << "width:#{width}px;" + html_id = "task-done-issue-#{options[:issue].id}" if options[:issue] + html_id = "task-done-version-#{options[:version].id}" if options[:version] + output << view.content_tag(:div, ' '.html_safe, + :style => style, + :class => "#{options[:css]} task_done", + :id => html_id) + end + end + # Renders the markers + if options[:markers] + if coords[:start] + style = "" + style << "top:#{params[:top]}px;" + style << "left:#{coords[:start]}px;" + style << "width:15px;" + output << view.content_tag(:div, ' '.html_safe, + :style => style, + :class => "#{options[:css]} marker starting") + end + if coords[:end] + style = "" + style << "top:#{params[:top]}px;" + style << "left:#{coords[:end] + params[:zoom]}px;" + style << "width:15px;" + output << view.content_tag(:div, ' '.html_safe, + :style => style, + :class => "#{options[:css]} marker ending") + end + end + # Renders the label on the right + if options[:label] + style = "" + style << "top:#{params[:top]}px;" + style << "left:#{(coords[:bar_end] || 0) + 8}px;" + style << "width:15px;" + output << view.content_tag(:div, options[:label], + :style => style, + :class => "#{options[:css]} label") + end + # Renders the tooltip + if options[:issue] && coords[:bar_start] && coords[:bar_end] + s = view.content_tag(:span, + view.render_issue_tooltip(options[:issue]).html_safe, + :class => "tip") + style = "" + style << "position: absolute;" + style << "top:#{params[:top]}px;" + style << "left:#{coords[:bar_start]}px;" + style << "width:#{coords[:bar_end] - coords[:bar_start]}px;" + style << "height:12px;" + output << view.content_tag(:div, s.html_safe, + :style => style, + :class => "tooltip") + end + @lines << output + output + end + + def pdf_task(params, coords, options={}) + height = options[:height] || 2 + # Renders the task bar, with progress and late + if coords[:bar_start] && coords[:bar_end] + params[:pdf].SetY(params[:top] + 1.5) + params[:pdf].SetX(params[:subject_width] + coords[:bar_start]) + params[:pdf].SetFillColor(200, 200, 200) + params[:pdf].RDMCell(coords[:bar_end] - coords[:bar_start], height, "", 0, 0, "", 1) + if coords[:bar_late_end] + params[:pdf].SetY(params[:top] + 1.5) + params[:pdf].SetX(params[:subject_width] + coords[:bar_start]) + params[:pdf].SetFillColor(255, 100, 100) + params[:pdf].RDMCell(coords[:bar_late_end] - coords[:bar_start], height, "", 0, 0, "", 1) + end + if coords[:bar_progress_end] + params[:pdf].SetY(params[:top] + 1.5) + params[:pdf].SetX(params[:subject_width] + coords[:bar_start]) + params[:pdf].SetFillColor(90, 200, 90) + params[:pdf].RDMCell(coords[:bar_progress_end] - coords[:bar_start], height, "", 0, 0, "", 1) + end + end + # Renders the markers + if options[:markers] + if coords[:start] + params[:pdf].SetY(params[:top] + 1) + params[:pdf].SetX(params[:subject_width] + coords[:start] - 1) + params[:pdf].SetFillColor(50, 50, 200) + params[:pdf].RDMCell(2, 2, "", 0, 0, "", 1) + end + if coords[:end] + params[:pdf].SetY(params[:top] + 1) + params[:pdf].SetX(params[:subject_width] + coords[:end] - 1) + params[:pdf].SetFillColor(50, 50, 200) + params[:pdf].RDMCell(2, 2, "", 0, 0, "", 1) + end + end + # Renders the label on the right + if options[:label] + params[:pdf].SetX(params[:subject_width] + (coords[:bar_end] || 0) + 5) + params[:pdf].RDMCell(30, 2, options[:label]) + end + end + + def image_task(params, coords, options={}) + height = options[:height] || 6 + # Renders the task bar, with progress and late + if coords[:bar_start] && coords[:bar_end] + params[:image].fill('#aaa') + params[:image].rectangle(params[:subject_width] + coords[:bar_start], + params[:top], + params[:subject_width] + coords[:bar_end], + params[:top] - height) + if coords[:bar_late_end] + params[:image].fill('#f66') + params[:image].rectangle(params[:subject_width] + coords[:bar_start], + params[:top], + params[:subject_width] + coords[:bar_late_end], + params[:top] - height) + end + if coords[:bar_progress_end] + params[:image].fill('#00c600') + params[:image].rectangle(params[:subject_width] + coords[:bar_start], + params[:top], + params[:subject_width] + coords[:bar_progress_end], + params[:top] - height) + end + end + # Renders the markers + if options[:markers] + if coords[:start] + x = params[:subject_width] + coords[:start] + y = params[:top] - height / 2 + params[:image].fill('blue') + params[:image].polygon(x - 4, y, x, y - 4, x + 4, y, x, y + 4) + end + if coords[:end] + x = params[:subject_width] + coords[:end] + params[:zoom] + y = params[:top] - height / 2 + params[:image].fill('blue') + params[:image].polygon(x - 4, y, x, y - 4, x + 4, y, x, y + 4) + end + end + # Renders the label on the right + if options[:label] + params[:image].fill('black') + params[:image].text(params[:subject_width] + (coords[:bar_end] || 0) + 5, + params[:top] + 1, + options[:label]) + end + end + end + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/02/027410b40ae4e2d4f4be0368cf13b4cdb0164b5f.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/02/027410b40ae4e2d4f4be0368cf13b4cdb0164b5f.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,129 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 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 File.expand_path('../../test_helper', __FILE__) + +class RoleTest < ActiveSupport::TestCase + fixtures :roles, :workflows, :trackers + + def test_sorted_scope + assert_equal Role.all.sort, Role.sorted.all + end + + def test_givable_scope + assert_equal Role.all.reject(&:builtin?).sort, Role.givable.all + end + + def test_builtin_scope + assert_equal Role.all.select(&:builtin?).sort, Role.builtin(true).all.sort + assert_equal Role.all.reject(&:builtin?).sort, Role.builtin(false).all.sort + end + + def test_copy_from + role = Role.find(1) + copy = Role.new.copy_from(role) + + assert_nil copy.id + assert_equal '', copy.name + assert_equal role.permissions, copy.permissions + + copy.name = 'Copy' + assert copy.save + end + + def test_copy_workflows + source = Role.find(1) + assert_equal 90, source.workflow_rules.size + + target = Role.new(:name => 'Target') + assert target.save + target.workflow_rules.copy(source) + target.reload + assert_equal 90, target.workflow_rules.size + end + + def test_permissions_should_be_unserialized_with_its_coder + Role::PermissionsAttributeCoder.expects(:load).once + Role.find(1).permissions + end + + def test_add_permission + role = Role.find(1) + size = role.permissions.size + role.add_permission!("apermission", "anotherpermission") + role.reload + assert role.permissions.include?(:anotherpermission) + assert_equal size + 2, role.permissions.size + end + + def test_remove_permission + role = Role.find(1) + size = role.permissions.size + perm = role.permissions[0..1] + role.remove_permission!(*perm) + role.reload + assert ! role.permissions.include?(perm[0]) + assert_equal size - 2, role.permissions.size + end + + def test_name + I18n.locale = 'fr' + assert_equal 'Manager', Role.find(1).name + assert_equal 'Anonyme', Role.anonymous.name + assert_equal 'Non membre', Role.non_member.name + end + + def test_find_all_givable + assert_equal Role.all.reject(&:builtin?).sort, Role.find_all_givable + end + + def test_anonymous_should_return_the_anonymous_role + assert_no_difference('Role.count') do + role = Role.anonymous + assert role.builtin? + assert_equal Role::BUILTIN_ANONYMOUS, role.builtin + end + end + + def test_anonymous_with_a_missing_anonymous_role_should_return_the_anonymous_role + Role.where(:builtin => Role::BUILTIN_ANONYMOUS).delete_all + + assert_difference('Role.count') do + role = Role.anonymous + assert role.builtin? + assert_equal Role::BUILTIN_ANONYMOUS, role.builtin + end + end + + def test_non_member_should_return_the_non_member_role + assert_no_difference('Role.count') do + role = Role.non_member + assert role.builtin? + assert_equal Role::BUILTIN_NON_MEMBER, role.builtin + end + end + + def test_non_member_with_a_missing_non_member_role_should_return_the_non_member_role + Role.where(:builtin => Role::BUILTIN_NON_MEMBER).delete_all + + assert_difference('Role.count') do + role = Role.non_member + assert role.builtin? + assert_equal Role::BUILTIN_NON_MEMBER, role.builtin + end + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/02/02c68441083bdea630158440f0e7d9d62ff8f790.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/02/02c68441083bdea630158440f0e7d9d62ff8f790.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,822 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2013 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 File.expand_path('../../test_helper', __FILE__) + +class MailHandlerTest < ActiveSupport::TestCase + fixtures :users, :projects, :enabled_modules, :roles, + :members, :member_roles, :users, + :issues, :issue_statuses, + :workflows, :trackers, :projects_trackers, + :versions, :enumerations, :issue_categories, + :custom_fields, :custom_fields_trackers, :custom_fields_projects, + :boards, :messages + + FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler' + + def setup + ActionMailer::Base.deliveries.clear + Setting.notified_events = Redmine::Notifiable.all.collect(&:name) + end + + def teardown + Setting.clear_cache + end + + def test_add_issue + ActionMailer::Base.deliveries.clear + # This email contains: 'Project: onlinestore' + issue = submit_email('ticket_on_given_project.eml') + assert issue.is_a?(Issue) + assert !issue.new_record? + issue.reload + assert_equal Project.find(2), issue.project + assert_equal issue.project.trackers.first, issue.tracker + assert_equal 'New ticket on a given project', issue.subject + assert_equal User.find_by_login('jsmith'), issue.author + assert_equal IssueStatus.find_by_name('Resolved'), issue.status + assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') + assert_equal '2010-01-01', issue.start_date.to_s + assert_equal '2010-12-31', issue.due_date.to_s + assert_equal User.find_by_login('jsmith'), issue.assigned_to + assert_equal Version.find_by_name('Alpha'), issue.fixed_version + assert_equal 2.5, issue.estimated_hours + assert_equal 30, issue.done_ratio + assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt] + # keywords should be removed from the email body + assert !issue.description.match(/^Project:/i) + assert !issue.description.match(/^Status:/i) + assert !issue.description.match(/^Start Date:/i) + # Email notification should be sent + mail = ActionMailer::Base.deliveries.last + assert_not_nil mail + assert mail.subject.include?('New ticket on a given project') + end + + def test_add_issue_with_default_tracker + # This email contains: 'Project: onlinestore' + issue = submit_email( + 'ticket_on_given_project.eml', + :issue => {:tracker => 'Support request'} + ) + assert issue.is_a?(Issue) + assert !issue.new_record? + issue.reload + assert_equal 'Support request', issue.tracker.name + end + + def test_add_issue_with_status + # This email contains: 'Project: onlinestore' and 'Status: Resolved' + issue = submit_email('ticket_on_given_project.eml') + assert issue.is_a?(Issue) + assert !issue.new_record? + issue.reload + assert_equal Project.find(2), issue.project + assert_equal IssueStatus.find_by_name("Resolved"), issue.status + end + + def test_add_issue_with_attributes_override + issue = submit_email( + 'ticket_with_attributes.eml', + :allow_override => 'tracker,category,priority' + ) + assert issue.is_a?(Issue) + assert !issue.new_record? + issue.reload + assert_equal 'New ticket on a given project', issue.subject + assert_equal User.find_by_login('jsmith'), issue.author + assert_equal Project.find(2), issue.project + assert_equal 'Feature request', issue.tracker.to_s + assert_equal 'Stock management', issue.category.to_s + assert_equal 'Urgent', issue.priority.to_s + assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') + end + + def test_add_issue_with_group_assignment + with_settings :issue_group_assignment => '1' do + issue = submit_email('ticket_on_given_project.eml') do |email| + email.gsub!('Assigned to: John Smith', 'Assigned to: B Team') + end + assert issue.is_a?(Issue) + assert !issue.new_record? + issue.reload + assert_equal Group.find(11), issue.assigned_to + end + end + + def test_add_issue_with_partial_attributes_override + issue = submit_email( + 'ticket_with_attributes.eml', + :issue => {:priority => 'High'}, + :allow_override => ['tracker'] + ) + assert issue.is_a?(Issue) + assert !issue.new_record? + issue.reload + assert_equal 'New ticket on a given project', issue.subject + assert_equal User.find_by_login('jsmith'), issue.author + assert_equal Project.find(2), issue.project + assert_equal 'Feature request', issue.tracker.to_s + assert_nil issue.category + assert_equal 'High', issue.priority.to_s + assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') + end + + def test_add_issue_with_spaces_between_attribute_and_separator + issue = submit_email( + 'ticket_with_spaces_between_attribute_and_separator.eml', + :allow_override => 'tracker,category,priority' + ) + assert issue.is_a?(Issue) + assert !issue.new_record? + issue.reload + assert_equal 'New ticket on a given project', issue.subject + assert_equal User.find_by_login('jsmith'), issue.author + assert_equal Project.find(2), issue.project + assert_equal 'Feature request', issue.tracker.to_s + assert_equal 'Stock management', issue.category.to_s + assert_equal 'Urgent', issue.priority.to_s + assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') + end + + def test_add_issue_with_attachment_to_specific_project + issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'}) + assert issue.is_a?(Issue) + assert !issue.new_record? + issue.reload + assert_equal 'Ticket created by email with attachment', issue.subject + assert_equal User.find_by_login('jsmith'), issue.author + assert_equal Project.find(2), issue.project + assert_equal 'This is a new ticket with attachments', issue.description + # Attachment properties + assert_equal 1, issue.attachments.size + assert_equal 'Paella.jpg', issue.attachments.first.filename + assert_equal 'image/jpeg', issue.attachments.first.content_type + assert_equal 10790, issue.attachments.first.filesize + end + + def test_add_issue_with_custom_fields + issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'}) + assert issue.is_a?(Issue) + assert !issue.new_record? + issue.reload + assert_equal 'New ticket with custom field values', issue.subject + assert_equal 'PostgreSQL', issue.custom_field_value(1) + assert_equal 'Value for a custom field', issue.custom_field_value(2) + assert !issue.description.match(/^searchable field:/i) + end + + def test_add_issue_with_version_custom_fields + field = IssueCustomField.create!(:name => 'Affected version', :field_format => 'version', :is_for_all => true, :tracker_ids => [1,2,3]) + + issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'ecookbook'}) do |email| + email << "Affected version: 1.0\n" + end + assert issue.is_a?(Issue) + assert !issue.new_record? + issue.reload + assert_equal '2', issue.custom_field_value(field) + end + + def test_add_issue_should_match_assignee_on_display_name + user = User.generate!(:firstname => 'Foo Bar', :lastname => 'Foo Baz') + User.add_to_project(user, Project.find(2)) + issue = submit_email('ticket_on_given_project.eml') do |email| + email.sub!(/^Assigned to.*$/, 'Assigned to: Foo Bar Foo baz') + end + assert issue.is_a?(Issue) + assert_equal user, issue.assigned_to + end + + def test_add_issue_with_cc + issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'}) + assert issue.is_a?(Issue) + assert !issue.new_record? + issue.reload + assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo')) + assert_equal 1, issue.watcher_user_ids.size + end + + def test_add_issue_by_unknown_user + assert_no_difference 'User.count' do + assert_equal false, + submit_email( + 'ticket_by_unknown_user.eml', + :issue => {:project => 'ecookbook'} + ) + end + end + + def test_add_issue_by_anonymous_user + Role.anonymous.add_permission!(:add_issues) + assert_no_difference 'User.count' do + issue = submit_email( + 'ticket_by_unknown_user.eml', + :issue => {:project => 'ecookbook'}, + :unknown_user => 'accept' + ) + assert issue.is_a?(Issue) + assert issue.author.anonymous? + end + end + + def test_add_issue_by_anonymous_user_with_no_from_address + Role.anonymous.add_permission!(:add_issues) + assert_no_difference 'User.count' do + issue = submit_email( + 'ticket_by_empty_user.eml', + :issue => {:project => 'ecookbook'}, + :unknown_user => 'accept' + ) + assert issue.is_a?(Issue) + assert issue.author.anonymous? + end + end + + def test_add_issue_by_anonymous_user_on_private_project + Role.anonymous.add_permission!(:add_issues) + assert_no_difference 'User.count' do + assert_no_difference 'Issue.count' do + assert_equal false, + submit_email( + 'ticket_by_unknown_user.eml', + :issue => {:project => 'onlinestore'}, + :unknown_user => 'accept' + ) + end + end + end + + def test_add_issue_by_anonymous_user_on_private_project_without_permission_check + assert_no_difference 'User.count' do + assert_difference 'Issue.count' do + issue = submit_email( + 'ticket_by_unknown_user.eml', + :issue => {:project => 'onlinestore'}, + :no_permission_check => '1', + :unknown_user => 'accept' + ) + assert issue.is_a?(Issue) + assert issue.author.anonymous? + assert !issue.project.is_public? + assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt] + end + end + end + + def test_add_issue_by_created_user + Setting.default_language = 'en' + assert_difference 'User.count' do + issue = submit_email( + 'ticket_by_unknown_user.eml', + :issue => {:project => 'ecookbook'}, + :unknown_user => 'create' + ) + assert issue.is_a?(Issue) + assert issue.author.active? + assert_equal 'john.doe@somenet.foo', issue.author.mail + assert_equal 'John', issue.author.firstname + assert_equal 'Doe', issue.author.lastname + + # account information + email = ActionMailer::Base.deliveries.first + assert_not_nil email + assert email.subject.include?('account activation') + login = mail_body(email).match(/\* Login: (.*)$/)[1].strip + password = mail_body(email).match(/\* Password: (.*)$/)[1].strip + assert_equal issue.author, User.try_to_login(login, password) + end + end + + def test_created_user_should_be_added_to_groups + group1 = Group.generate! + group2 = Group.generate! + + assert_difference 'User.count' do + submit_email( + 'ticket_by_unknown_user.eml', + :issue => {:project => 'ecookbook'}, + :unknown_user => 'create', + :default_group => "#{group1.name},#{group2.name}" + ) + end + user = User.order('id DESC').first + assert_same_elements [group1, group2], user.groups + end + + def test_created_user_should_not_receive_account_information_with_no_account_info_option + assert_difference 'User.count' do + submit_email( + 'ticket_by_unknown_user.eml', + :issue => {:project => 'ecookbook'}, + :unknown_user => 'create', + :no_account_notice => '1' + ) + end + + # only 1 email for the new issue notification + assert_equal 1, ActionMailer::Base.deliveries.size + email = ActionMailer::Base.deliveries.first + assert_include 'Ticket by unknown user', email.subject + end + + def test_created_user_should_have_mail_notification_to_none_with_no_notification_option + assert_difference 'User.count' do + submit_email( + 'ticket_by_unknown_user.eml', + :issue => {:project => 'ecookbook'}, + :unknown_user => 'create', + :no_notification => '1' + ) + end + user = User.order('id DESC').first + assert_equal 'none', user.mail_notification + end + + def test_add_issue_without_from_header + Role.anonymous.add_permission!(:add_issues) + assert_equal false, submit_email('ticket_without_from_header.eml') + end + + def test_add_issue_with_invalid_attributes + issue = submit_email( + 'ticket_with_invalid_attributes.eml', + :allow_override => 'tracker,category,priority' + ) + assert issue.is_a?(Issue) + assert !issue.new_record? + issue.reload + assert_nil issue.assigned_to + assert_nil issue.start_date + assert_nil issue.due_date + assert_equal 0, issue.done_ratio + assert_equal 'Normal', issue.priority.to_s + assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') + end + + def test_add_issue_with_localized_attributes + User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr' + issue = submit_email( + 'ticket_with_localized_attributes.eml', + :allow_override => 'tracker,category,priority' + ) + assert issue.is_a?(Issue) + assert !issue.new_record? + issue.reload + assert_equal 'New ticket on a given project', issue.subject + assert_equal User.find_by_login('jsmith'), issue.author + assert_equal Project.find(2), issue.project + assert_equal 'Feature request', issue.tracker.to_s + assert_equal 'Stock management', issue.category.to_s + assert_equal 'Urgent', issue.priority.to_s + assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') + end + + def test_add_issue_with_japanese_keywords + ja_dev = "\xe9\x96\x8b\xe7\x99\xba" + ja_dev.force_encoding('UTF-8') if ja_dev.respond_to?(:force_encoding) + tracker = Tracker.create!(:name => ja_dev) + Project.find(1).trackers << tracker + issue = submit_email( + 'japanese_keywords_iso_2022_jp.eml', + :issue => {:project => 'ecookbook'}, + :allow_override => 'tracker' + ) + assert_kind_of Issue, issue + assert_equal tracker, issue.tracker + end + + def test_add_issue_from_apple_mail + issue = submit_email( + 'apple_mail_with_attachment.eml', + :issue => {:project => 'ecookbook'} + ) + assert_kind_of Issue, issue + assert_equal 1, issue.attachments.size + + attachment = issue.attachments.first + assert_equal 'paella.jpg', attachment.filename + assert_equal 10790, attachment.filesize + assert File.exist?(attachment.diskfile) + assert_equal 10790, File.size(attachment.diskfile) + assert_equal 'caaf384198bcbc9563ab5c058acd73cd', attachment.digest + end + + def test_thunderbird_with_attachment_ja + issue = submit_email( + 'thunderbird_with_attachment_ja.eml', + :issue => {:project => 'ecookbook'} + ) + assert_kind_of Issue, issue + assert_equal 1, issue.attachments.size + ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt" + ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding) + attachment = issue.attachments.first + assert_equal ja, attachment.filename + assert_equal 5, attachment.filesize + assert File.exist?(attachment.diskfile) + assert_equal 5, File.size(attachment.diskfile) + assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest + end + + def test_gmail_with_attachment_ja + issue = submit_email( + 'gmail_with_attachment_ja.eml', + :issue => {:project => 'ecookbook'} + ) + assert_kind_of Issue, issue + assert_equal 1, issue.attachments.size + ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt" + ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding) + attachment = issue.attachments.first + assert_equal ja, attachment.filename + assert_equal 5, attachment.filesize + assert File.exist?(attachment.diskfile) + assert_equal 5, File.size(attachment.diskfile) + assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest + end + + def test_thunderbird_with_attachment_latin1 + issue = submit_email( + 'thunderbird_with_attachment_iso-8859-1.eml', + :issue => {:project => 'ecookbook'} + ) + assert_kind_of Issue, issue + assert_equal 1, issue.attachments.size + u = "" + u.force_encoding('UTF-8') if u.respond_to?(:force_encoding) + u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc" + u1.force_encoding('UTF-8') if u1.respond_to?(:force_encoding) + 11.times { u << u1 } + attachment = issue.attachments.first + assert_equal "#{u}.png", attachment.filename + assert_equal 130, attachment.filesize + assert File.exist?(attachment.diskfile) + assert_equal 130, File.size(attachment.diskfile) + assert_equal '4d80e667ac37dddfe05502530f152abb', attachment.digest + end + + def test_gmail_with_attachment_latin1 + issue = submit_email( + 'gmail_with_attachment_iso-8859-1.eml', + :issue => {:project => 'ecookbook'} + ) + assert_kind_of Issue, issue + assert_equal 1, issue.attachments.size + u = "" + u.force_encoding('UTF-8') if u.respond_to?(:force_encoding) + u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc" + u1.force_encoding('UTF-8') if u1.respond_to?(:force_encoding) + 11.times { u << u1 } + attachment = issue.attachments.first + assert_equal "#{u}.txt", attachment.filename + assert_equal 5, attachment.filesize + assert File.exist?(attachment.diskfile) + assert_equal 5, File.size(attachment.diskfile) + assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest + end + + def test_add_issue_with_iso_8859_1_subject + issue = submit_email( + 'subject_as_iso-8859-1.eml', + :issue => {:project => 'ecookbook'} + ) + str = "Testmail from Webmail: \xc3\xa4 \xc3\xb6 \xc3\xbc..." + str.force_encoding('UTF-8') if str.respond_to?(:force_encoding) + assert_kind_of Issue, issue + assert_equal str, issue.subject + end + + def test_add_issue_with_japanese_subject + issue = submit_email( + 'subject_japanese_1.eml', + :issue => {:project => 'ecookbook'} + ) + assert_kind_of Issue, issue + ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88" + ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding) + assert_equal ja, issue.subject + end + + def test_add_issue_with_no_subject_header + issue = submit_email( + 'no_subject_header.eml', + :issue => {:project => 'ecookbook'} + ) + assert_kind_of Issue, issue + assert_equal '(no subject)', issue.subject + end + + def test_add_issue_with_mixed_japanese_subject + issue = submit_email( + 'subject_japanese_2.eml', + :issue => {:project => 'ecookbook'} + ) + assert_kind_of Issue, issue + ja = "Re: \xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88" + ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding) + assert_equal ja, issue.subject + end + + def test_should_ignore_emails_from_locked_users + User.find(2).lock! + + MailHandler.any_instance.expects(:dispatch).never + assert_no_difference 'Issue.count' do + assert_equal false, submit_email('ticket_on_given_project.eml') + end + end + + def test_should_ignore_emails_from_emission_address + Role.anonymous.add_permission!(:add_issues) + assert_no_difference 'User.count' do + assert_equal false, + submit_email( + 'ticket_from_emission_address.eml', + :issue => {:project => 'ecookbook'}, + :unknown_user => 'create' + ) + end + end + + def test_should_ignore_auto_replied_emails + MailHandler.any_instance.expects(:dispatch).never + [ + "X-Auto-Response-Suppress: OOF", + "Auto-Submitted: auto-replied", + "Auto-Submitted: Auto-Replied", + "Auto-Submitted: auto-generated" + ].each do |header| + raw = IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml')) + raw = header + "\n" + raw + + assert_no_difference 'Issue.count' do + assert_equal false, MailHandler.receive(raw), "email with #{header} header was not ignored" + end + end + end + + def test_add_issue_should_send_email_notification + Setting.notified_events = ['issue_added'] + ActionMailer::Base.deliveries.clear + # This email contains: 'Project: onlinestore' + issue = submit_email('ticket_on_given_project.eml') + assert issue.is_a?(Issue) + assert_equal 1, ActionMailer::Base.deliveries.size + end + + def test_update_issue + journal = submit_email('ticket_reply.eml') + assert journal.is_a?(Journal) + assert_equal User.find_by_login('jsmith'), journal.user + assert_equal Issue.find(2), journal.journalized + assert_match /This is reply/, journal.notes + assert_equal false, journal.private_notes + assert_equal 'Feature request', journal.issue.tracker.name + end + + def test_update_issue_with_attribute_changes + # This email contains: 'Status: Resolved' + journal = submit_email('ticket_reply_with_status.eml') + assert journal.is_a?(Journal) + issue = Issue.find(journal.issue.id) + assert_equal User.find_by_login('jsmith'), journal.user + assert_equal Issue.find(2), journal.journalized + assert_match /This is reply/, journal.notes + assert_equal 'Feature request', journal.issue.tracker.name + assert_equal IssueStatus.find_by_name("Resolved"), issue.status + assert_equal '2010-01-01', issue.start_date.to_s + assert_equal '2010-12-31', issue.due_date.to_s + assert_equal User.find_by_login('jsmith'), issue.assigned_to + assert_equal "52.6", issue.custom_value_for(CustomField.find_by_name('Float field')).value + # keywords should be removed from the email body + assert !journal.notes.match(/^Status:/i) + assert !journal.notes.match(/^Start Date:/i) + end + + def test_update_issue_with_attachment + assert_difference 'Journal.count' do + assert_difference 'JournalDetail.count' do + assert_difference 'Attachment.count' do + assert_no_difference 'Issue.count' do + journal = submit_email('ticket_with_attachment.eml') do |raw| + raw.gsub! /^Subject: .*$/, 'Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories' + end + end + end + end + end + journal = Journal.first(:order => 'id DESC') + assert_equal Issue.find(2), journal.journalized + assert_equal 1, journal.details.size + + detail = journal.details.first + assert_equal 'attachment', detail.property + assert_equal 'Paella.jpg', detail.value + end + + def test_update_issue_should_send_email_notification + ActionMailer::Base.deliveries.clear + journal = submit_email('ticket_reply.eml') + assert journal.is_a?(Journal) + assert_equal 1, ActionMailer::Base.deliveries.size + end + + def test_update_issue_should_not_set_defaults + journal = submit_email( + 'ticket_reply.eml', + :issue => {:tracker => 'Support request', :priority => 'High'} + ) + assert journal.is_a?(Journal) + assert_match /This is reply/, journal.notes + assert_equal 'Feature request', journal.issue.tracker.name + assert_equal 'Normal', journal.issue.priority.name + end + + def test_replying_to_a_private_note_should_add_reply_as_private + private_journal = Journal.create!(:notes => 'Private notes', :journalized => Issue.find(1), :private_notes => true, :user_id => 2) + + assert_difference 'Journal.count' do + journal = submit_email('ticket_reply.eml') do |email| + email.sub! %r{^In-Reply-To:.*$}, "In-Reply-To:<%= l(:field_name) %> | +<%= l(:field_is_default) %> | +<%= l(:field_active) %> | +<%=l(:button_sort)%> | ++ |
---|---|---|---|---|
<%= link_to h(enumeration), edit_enumeration_path(enumeration) %> | +<%= checked_image enumeration.is_default? %> | +<%= checked_image enumeration.active? %> | +<%= reorder_links('enumeration', {:action => 'update', :id => enumeration}, :put) %> | +<%= delete_link enumeration_path(enumeration) %> | +
<%= link_to l(:label_enumeration_new), new_enumeration_path(:type => klass.name) %>
+<% end %> + +<% html_title(l(:label_enumerations)) -%> diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/03/033d1080f94d03c5b655f6847c2f7606bd5d5f60.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/03/033d1080f94d03c5b655f6847c2f7606bd5d5f60.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,54 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2013 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. + +module UsersHelper + def users_status_options_for_select(selected) + user_count_by_status = User.count(:group => 'status').to_hash + options_for_select([[l(:label_all), ''], + ["#{l(:status_active)} (#{user_count_by_status[1].to_i})", '1'], + ["#{l(:status_registered)} (#{user_count_by_status[2].to_i})", '2'], + ["#{l(:status_locked)} (#{user_count_by_status[3].to_i})", '3']], selected.to_s) + end + + def user_mail_notification_options(user) + user.valid_notification_options.collect {|o| [l(o.last), o.first]} + end + + def change_status_link(user) + url = {:controller => 'users', :action => 'update', :id => user, :page => params[:page], :status => params[:status], :tab => nil} + + if user.locked? + link_to l(:button_unlock), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :put, :class => 'icon icon-unlock' + elsif user.registered? + link_to l(:button_activate), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :put, :class => 'icon icon-unlock' + elsif user != User.current + link_to l(:button_lock), url.merge(:user => {:status => User::STATUS_LOCKED}), :method => :put, :class => 'icon icon-lock' + end + end + + def user_settings_tabs + tabs = [{:name => 'general', :partial => 'users/general', :label => :label_general}, + {:name => 'memberships', :partial => 'users/memberships', :label => :label_project_plural} + ] + if Group.all.any? + tabs.insert 1, {:name => 'groups', :partial => 'users/groups', :label => :label_group_plural} + end + tabs + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/03/03b977fa002b4a1e6202e05fed1b5c5f4bee2ed3.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/03/03b977fa002b4a1e6202e05fed1b5c5f4bee2ed3.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,1029 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Project < ActiveRecord::Base + include Redmine::SafeAttributes + + # Project statuses + STATUS_ACTIVE = 1 + STATUS_CLOSED = 5 + STATUS_ARCHIVED = 9 + + # Maximum length for project identifiers + IDENTIFIER_MAX_LENGTH = 100 + + # Specific overidden Activities + has_many :time_entry_activities + has_many :members, :include => [:principal, :roles], :conditions => "#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE}" + has_many :memberships, :class_name => 'Member' + has_many :member_principals, :class_name => 'Member', + :include => :principal, + :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE})" + has_many :users, :through => :members + has_many :principals, :through => :member_principals, :source => :principal + + has_many :enabled_modules, :dependent => :delete_all + has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position" + has_many :issues, :dependent => :destroy, :include => [:status, :tracker] + has_many :issue_changes, :through => :issues, :source => :journals + has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC" + has_many :time_entries, :dependent => :delete_all + has_many :queries, :class_name => 'IssueQuery', :dependent => :delete_all + has_many :documents, :dependent => :destroy + has_many :news, :dependent => :destroy, :include => :author + has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name" + has_many :boards, :dependent => :destroy, :order => "position ASC" + has_one :repository, :conditions => ["is_default = ?", true] + has_many :repositories, :dependent => :destroy + has_many :changesets, :through => :repository + has_one :wiki, :dependent => :destroy + # Custom field for the project issues + has_and_belongs_to_many :issue_custom_fields, + :class_name => 'IssueCustomField', + :order => "#{CustomField.table_name}.position", + :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", + :association_foreign_key => 'custom_field_id' + + acts_as_nested_set :order => 'name', :dependent => :destroy + acts_as_attachable :view_permission => :view_files, + :delete_permission => :manage_files + + acts_as_customizable + acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil + acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"}, + :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}}, + :author => nil + + attr_protected :status + + validates_presence_of :name, :identifier + validates_uniqueness_of :identifier + validates_associated :repository, :wiki + validates_length_of :name, :maximum => 255 + validates_length_of :homepage, :maximum => 255 + validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH + # donwcase letters, digits, dashes but not digits only + validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? } + # reserved words + validates_exclusion_of :identifier, :in => %w( new ) + + after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?} + after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?} + before_destroy :delete_all_members + + scope :has_module, lambda {|mod| + where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s) + } + scope :active, lambda { where(:status => STATUS_ACTIVE) } + scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) } + scope :all_public, lambda { where(:is_public => true) } + scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) } + scope :allowed_to, lambda {|*args| + user = User.current + permission = nil + if args.first.is_a?(Symbol) + permission = args.shift + else + user = args.shift + permission = args.shift + end + where(Project.allowed_to_condition(user, permission, *args)) + } + scope :like, lambda {|arg| + if arg.blank? + where(nil) + else + pattern = "%#{arg.to_s.strip.downcase}%" + where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern) + end + } + + def initialize(attributes=nil, *args) + super + + initialized = (attributes || {}).stringify_keys + if !initialized.key?('identifier') && Setting.sequential_project_identifiers? + self.identifier = Project.next_identifier + end + if !initialized.key?('is_public') + self.is_public = Setting.default_projects_public? + end + if !initialized.key?('enabled_module_names') + self.enabled_module_names = Setting.default_projects_modules + end + if !initialized.key?('trackers') && !initialized.key?('tracker_ids') + default = Setting.default_projects_tracker_ids + if default.is_a?(Array) + self.trackers = Tracker.where(:id => default.map(&:to_i)).sorted.all + else + self.trackers = Tracker.sorted.all + end + end + end + + def identifier=(identifier) + super unless identifier_frozen? + end + + def identifier_frozen? + errors[:identifier].blank? && !(new_record? || identifier.blank?) + end + + # returns latest created projects + # non public projects will be returned only if user is a member of those + def self.latest(user=nil, count=5) + visible(user).limit(count).order("created_on DESC").all + end + + # Returns true if the project is visible to +user+ or to the current user. + def visible?(user=User.current) + user.allowed_to?(:view_project, self) + end + + # Returns a SQL conditions string used to find all projects visible by the specified user. + # + # Examples: + # Project.visible_condition(admin) => "projects.status = 1" + # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))" + # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))" + def self.visible_condition(user, options={}) + allowed_to_condition(user, :view_project, options) + end + + # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+ + # + # Valid options: + # * :project => limit the condition to project + # * :with_subprojects => limit the condition to project and its subprojects + # * :member => limit the condition to the user projects + def self.allowed_to_condition(user, permission, options={}) + perm = Redmine::AccessControl.permission(permission) + base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}") + if perm && perm.project_module + # If the permission belongs to a project module, make sure the module is enabled + base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')" + end + if options[:project] + project_statement = "#{Project.table_name}.id = #{options[:project].id}" + project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects] + base_statement = "(#{project_statement}) AND (#{base_statement})" + end + + if user.admin? + base_statement + else + statement_by_role = {} + unless options[:member] + role = user.logged? ? Role.non_member : Role.anonymous + if role.allowed_to?(permission) + statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}" + end + end + if user.logged? + user.projects_by_role.each do |role, projects| + if role.allowed_to?(permission) && projects.any? + statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})" + end + end + end + if statement_by_role.empty? + "1=0" + else + if block_given? + statement_by_role.each do |role, statement| + if s = yield(role, user) + statement_by_role[role] = "(#{statement} AND (#{s}))" + end + end + end + "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))" + end + end + end + + # Returns the Systemwide and project specific activities + def activities(include_inactive=false) + if include_inactive + return all_activities + else + return active_activities + end + end + + # Will create a new Project specific Activity or update an existing one + # + # This will raise a ActiveRecord::Rollback if the TimeEntryActivity + # does not successfully save. + def update_or_create_time_entry_activity(id, activity_hash) + if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id') + self.create_time_entry_activity_if_needed(activity_hash) + else + activity = project.time_entry_activities.find_by_id(id.to_i) + activity.update_attributes(activity_hash) if activity + end + end + + # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity + # + # This will raise a ActiveRecord::Rollback if the TimeEntryActivity + # does not successfully save. + def create_time_entry_activity_if_needed(activity) + if activity['parent_id'] + + parent_activity = TimeEntryActivity.find(activity['parent_id']) + activity['name'] = parent_activity.name + activity['position'] = parent_activity.position + + if Enumeration.overridding_change?(activity, parent_activity) + project_activity = self.time_entry_activities.create(activity) + + if project_activity.new_record? + raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved" + else + self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id]) + end + end + end + end + + # Returns a :conditions SQL string that can be used to find the issues associated with this project. + # + # Examples: + # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))" + # project.project_condition(false) => "projects.id = 1" + def project_condition(with_subprojects) + cond = "#{Project.table_name}.id = #{id}" + cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects + cond + end + + def self.find(*args) + if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/) + project = find_by_identifier(*args) + raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil? + project + else + super + end + end + + def self.find_by_param(*args) + self.find(*args) + end + + alias :base_reload :reload + def reload(*args) + @shared_versions = nil + @rolled_up_versions = nil + @rolled_up_trackers = nil + @all_issue_custom_fields = nil + @all_time_entry_custom_fields = nil + @to_param = nil + @allowed_parents = nil + @allowed_permissions = nil + @actions_allowed = nil + @start_date = nil + @due_date = nil + base_reload(*args) + end + + def to_param + # id is used for projects with a numeric identifier (compatibility) + @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier) + end + + def active? + self.status == STATUS_ACTIVE + end + + def archived? + self.status == STATUS_ARCHIVED + end + + # Archives the project and its descendants + def archive + # Check that there is no issue of a non descendant project that is assigned + # to one of the project or descendant versions + v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten + if v_ids.any? && + Issue. + includes(:project). + where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt). + where("#{Issue.table_name}.fixed_version_id IN (?)", v_ids). + exists? + return false + end + Project.transaction do + archive! + end + true + end + + # Unarchives the project + # All its ancestors must be active + def unarchive + return false if ancestors.detect {|a| !a.active?} + update_attribute :status, STATUS_ACTIVE + end + + def close + self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED + end + + def reopen + self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE + end + + # Returns an array of projects the project can be moved to + # by the current user + def allowed_parents + return @allowed_parents if @allowed_parents + @allowed_parents = Project.where(Project.allowed_to_condition(User.current, :add_subprojects)).all + @allowed_parents = @allowed_parents - self_and_descendants + if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?) + @allowed_parents << nil + end + unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent) + @allowed_parents << parent + end + @allowed_parents + end + + # Sets the parent of the project with authorization check + def set_allowed_parent!(p) + unless p.nil? || p.is_a?(Project) + if p.to_s.blank? + p = nil + else + p = Project.find_by_id(p) + return false unless p + end + end + if p.nil? + if !new_record? && allowed_parents.empty? + return false + end + elsif !allowed_parents.include?(p) + return false + end + set_parent!(p) + end + + # Sets the parent of the project + # Argument can be either a Project, a String, a Fixnum or nil + def set_parent!(p) + unless p.nil? || p.is_a?(Project) + if p.to_s.blank? + p = nil + else + p = Project.find_by_id(p) + return false unless p + end + end + if p == parent && !p.nil? + # Nothing to do + true + elsif p.nil? || (p.active? && move_possible?(p)) + set_or_update_position_under(p) + Issue.update_versions_from_hierarchy_change(self) + true + else + # Can not move to the given target + false + end + end + + # Recalculates all lft and rgt values based on project names + # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid + # Used in BuildProjectsTree migration + def self.rebuild_tree! + transaction do + update_all "lft = NULL, rgt = NULL" + rebuild!(false) + end + end + + # Returns an array of the trackers used by the project and its active sub projects + def rolled_up_trackers + @rolled_up_trackers ||= + Tracker. + joins(:projects). + joins("JOIN #{EnabledModule.table_name} ON #{EnabledModule.table_name}.project_id = #{Project.table_name}.id AND #{EnabledModule.table_name}.name = 'issue_tracking'"). + select("DISTINCT #{Tracker.table_name}.*"). + where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt). + sorted. + all + end + + # Closes open and locked project versions that are completed + def close_completed_versions + Version.transaction do + versions.where(:status => %w(open locked)).all.each do |version| + if version.completed? + version.update_attribute(:status, 'closed') + end + end + end + end + + # Returns a scope of the Versions on subprojects + def rolled_up_versions + @rolled_up_versions ||= + Version.scoped(:include => :project, + :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt]) + end + + # Returns a scope of the Versions used by the project + def shared_versions + if new_record? + Version.scoped(:include => :project, + :conditions => "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND #{Version.table_name}.sharing = 'system'") + else + @shared_versions ||= begin + r = root? ? self : root + Version.scoped(:include => :project, + :conditions => "#{Project.table_name}.id = #{id}" + + " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" + + " #{Version.table_name}.sharing = 'system'" + + " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" + + " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" + + " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" + + "))") + end + end + end + + # Returns a hash of project users grouped by role + def users_by_role + members.includes(:user, :roles).all.inject({}) do |h, m| + m.roles.each do |r| + h[r] ||= [] + h[r] << m.user + end + h + end + end + + # Deletes all project's members + def delete_all_members + me, mr = Member.table_name, MemberRole.table_name + connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})") + Member.delete_all(['project_id = ?', id]) + end + + # Users/groups issues can be assigned to + def assignable_users + assignable = Setting.issue_group_assignment? ? member_principals : members + assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort + end + + # Returns the mail adresses of users that should be always notified on project events + def recipients + notified_users.collect {|user| user.mail} + end + + # Returns the users that should be notified on project events + def notified_users + # TODO: User part should be extracted to User#notify_about? + members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal} + end + + # Returns an array of all custom fields enabled for project issues + # (explictly associated custom fields and custom fields enabled for all projects) + def all_issue_custom_fields + @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort + end + + # Returns an array of all custom fields enabled for project time entries + # (explictly associated custom fields and custom fields enabled for all projects) + def all_time_entry_custom_fields + @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort + end + + def project + self + end + + def <=>(project) + name.downcase <=> project.name.downcase + end + + def to_s + name + end + + # Returns a short description of the projects (first lines) + def short_description(length = 255) + description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description + end + + def css_classes + s = 'project' + s << ' root' if root? + s << ' child' if child? + s << (leaf? ? ' leaf' : ' parent') + unless active? + if archived? + s << ' archived' + else + s << ' closed' + end + end + s + end + + # The earliest start date of a project, based on it's issues and versions + def start_date + @start_date ||= [ + issues.minimum('start_date'), + shared_versions.minimum('effective_date'), + Issue.fixed_version(shared_versions).minimum('start_date') + ].compact.min + end + + # The latest due date of an issue or version + def due_date + @due_date ||= [ + issues.maximum('due_date'), + shared_versions.maximum('effective_date'), + Issue.fixed_version(shared_versions).maximum('due_date') + ].compact.max + end + + def overdue? + active? && !due_date.nil? && (due_date < Date.today) + end + + # Returns the percent completed for this project, based on the + # progress on it's versions. + def completed_percent(options={:include_subprojects => false}) + if options.delete(:include_subprojects) + total = self_and_descendants.collect(&:completed_percent).sum + + total / self_and_descendants.count + else + if versions.count > 0 + total = versions.collect(&:completed_percent).sum + + total / versions.count + else + 100 + end + end + end + + # Return true if this project allows to do the specified action. + # action can be: + # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit') + # * a permission Symbol (eg. :edit_project) + def allows_to?(action) + if archived? + # No action allowed on archived projects + return false + end + unless active? || Redmine::AccessControl.read_action?(action) + # No write action allowed on closed projects + return false + end + # No action allowed on disabled modules + if action.is_a? Hash + allowed_actions.include? "#{action[:controller]}/#{action[:action]}" + else + allowed_permissions.include? action + end + end + + def module_enabled?(module_name) + module_name = module_name.to_s + enabled_modules.detect {|m| m.name == module_name} + end + + def enabled_module_names=(module_names) + if module_names && module_names.is_a?(Array) + module_names = module_names.collect(&:to_s).reject(&:blank?) + self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)} + else + enabled_modules.clear + end + end + + # Returns an array of the enabled modules names + def enabled_module_names + enabled_modules.collect(&:name) + end + + # Enable a specific module + # + # Examples: + # project.enable_module!(:issue_tracking) + # project.enable_module!("issue_tracking") + def enable_module!(name) + enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name) + end + + # Disable a module if it exists + # + # Examples: + # project.disable_module!(:issue_tracking) + # project.disable_module!("issue_tracking") + # project.disable_module!(project.enabled_modules.first) + def disable_module!(target) + target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target) + target.destroy unless target.blank? + end + + safe_attributes 'name', + 'description', + 'homepage', + 'is_public', + 'identifier', + 'custom_field_values', + 'custom_fields', + 'tracker_ids', + 'issue_custom_field_ids' + + safe_attributes 'enabled_module_names', + :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) } + + safe_attributes 'inherit_members', + :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)} + + # Returns an array of projects that are in this project's hierarchy + # + # Example: parents, children, siblings + def hierarchy + parents = project.self_and_ancestors || [] + descendants = project.descendants || [] + project_hierarchy = parents | descendants # Set union + end + + # Returns an auto-generated project identifier based on the last identifier used + def self.next_identifier + p = Project.order('id DESC').first + p.nil? ? nil : p.identifier.to_s.succ + end + + # Copies and saves the Project instance based on the +project+. + # Duplicates the source project's: + # * Wiki + # * Versions + # * Categories + # * Issues + # * Members + # * Queries + # + # Accepts an +options+ argument to specify what to copy + # + # Examples: + # project.copy(1) # => copies everything + # project.copy(1, :only => 'members') # => copies members only + # project.copy(1, :only => ['members', 'versions']) # => copies members and versions + def copy(project, options={}) + project = project.is_a?(Project) ? project : Project.find(project) + + to_be_copied = %w(wiki versions issue_categories issues members queries boards) + to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil? + + Project.transaction do + if save + reload + to_be_copied.each do |name| + send "copy_#{name}", project + end + Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self) + save + end + end + end + + # Returns a new unsaved Project instance with attributes copied from +project+ + def self.copy_from(project) + project = project.is_a?(Project) ? project : Project.find(project) + # clear unique attributes + attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt') + copy = Project.new(attributes) + copy.enabled_modules = project.enabled_modules + copy.trackers = project.trackers + copy.custom_values = project.custom_values.collect {|v| v.clone} + copy.issue_custom_fields = project.issue_custom_fields + copy + end + + # Yields the given block for each project with its level in the tree + def self.project_tree(projects, &block) + ancestors = [] + projects.sort_by(&:lft).each do |project| + while (ancestors.any? && !project.is_descendant_of?(ancestors.last)) + ancestors.pop + end + yield project, ancestors.size + ancestors << project + end + end + + private + + def after_parent_changed(parent_was) + remove_inherited_member_roles + add_inherited_member_roles + end + + def update_inherited_members + if parent + if inherit_members? && !inherit_members_was + remove_inherited_member_roles + add_inherited_member_roles + elsif !inherit_members? && inherit_members_was + remove_inherited_member_roles + end + end + end + + def remove_inherited_member_roles + member_roles = memberships.map(&:member_roles).flatten + member_role_ids = member_roles.map(&:id) + member_roles.each do |member_role| + if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from) + member_role.destroy + end + end + end + + def add_inherited_member_roles + if inherit_members? && parent + parent.memberships.each do |parent_member| + member = Member.find_or_new(self.id, parent_member.user_id) + parent_member.member_roles.each do |parent_member_role| + member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id) + end + member.save! + end + end + end + + # Copies wiki from +project+ + def copy_wiki(project) + # Check that the source project has a wiki first + unless project.wiki.nil? + wiki = self.wiki || Wiki.new + wiki.attributes = project.wiki.attributes.dup.except("id", "project_id") + wiki_pages_map = {} + project.wiki.pages.each do |page| + # Skip pages without content + next if page.content.nil? + new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on")) + new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id")) + new_wiki_page.content = new_wiki_content + wiki.pages << new_wiki_page + wiki_pages_map[page.id] = new_wiki_page + end + + self.wiki = wiki + wiki.save + # Reproduce page hierarchy + project.wiki.pages.each do |page| + if page.parent_id && wiki_pages_map[page.id] + wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id] + wiki_pages_map[page.id].save + end + end + end + end + + # Copies versions from +project+ + def copy_versions(project) + project.versions.each do |version| + new_version = Version.new + new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on") + self.versions << new_version + end + end + + # Copies issue categories from +project+ + def copy_issue_categories(project) + project.issue_categories.each do |issue_category| + new_issue_category = IssueCategory.new + new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id") + self.issue_categories << new_issue_category + end + end + + # Copies issues from +project+ + def copy_issues(project) + # Stores the source issue id as a key and the copied issues as the + # value. Used to map the two togeather for issue relations. + issues_map = {} + + # Store status and reopen locked/closed versions + version_statuses = versions.reject(&:open?).map {|version| [version, version.status]} + version_statuses.each do |version, status| + version.update_attribute :status, 'open' + end + + # Get issues sorted by root_id, lft so that parent issues + # get copied before their children + project.issues.reorder('root_id, lft').all.each do |issue| + new_issue = Issue.new + new_issue.copy_from(issue, :subtasks => false, :link => false) + new_issue.project = self + # Changing project resets the custom field values + # TODO: handle this in Issue#project= + new_issue.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h} + # Reassign fixed_versions by name, since names are unique per project + if issue.fixed_version && issue.fixed_version.project == project + new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name} + end + # Reassign the category by name, since names are unique per project + if issue.category + new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name} + end + # Parent issue + if issue.parent_id + if copied_parent = issues_map[issue.parent_id] + new_issue.parent_issue_id = copied_parent.id + end + end + + self.issues << new_issue + if new_issue.new_record? + logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info + else + issues_map[issue.id] = new_issue unless new_issue.new_record? + end + end + + # Restore locked/closed version statuses + version_statuses.each do |version, status| + version.update_attribute :status, status + end + + # Relations after in case issues related each other + project.issues.each do |issue| + new_issue = issues_map[issue.id] + unless new_issue + # Issue was not copied + next + end + + # Relations + issue.relations_from.each do |source_relation| + new_issue_relation = IssueRelation.new + new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id") + new_issue_relation.issue_to = issues_map[source_relation.issue_to_id] + if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations? + new_issue_relation.issue_to = source_relation.issue_to + end + new_issue.relations_from << new_issue_relation + end + + issue.relations_to.each do |source_relation| + new_issue_relation = IssueRelation.new + new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id") + new_issue_relation.issue_from = issues_map[source_relation.issue_from_id] + if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations? + new_issue_relation.issue_from = source_relation.issue_from + end + new_issue.relations_to << new_issue_relation + end + end + end + + # Copies members from +project+ + def copy_members(project) + # Copy users first, then groups to handle members with inherited and given roles + members_to_copy = [] + members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)} + members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)} + + members_to_copy.each do |member| + new_member = Member.new + new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on") + # only copy non inherited roles + # inherited roles will be added when copying the group membership + role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id) + next if role_ids.empty? + new_member.role_ids = role_ids + new_member.project = self + self.members << new_member + end + end + + # Copies queries from +project+ + def copy_queries(project) + project.queries.each do |query| + new_query = IssueQuery.new + new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria") + new_query.sort_criteria = query.sort_criteria if query.sort_criteria + new_query.project = self + new_query.user_id = query.user_id + self.queries << new_query + end + end + + # Copies boards from +project+ + def copy_boards(project) + project.boards.each do |board| + new_board = Board.new + new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id") + new_board.project = self + self.boards << new_board + end + end + + def allowed_permissions + @allowed_permissions ||= begin + module_names = enabled_modules.all(:select => :name).collect {|m| m.name} + Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name} + end + end + + def allowed_actions + @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten + end + + # Returns all the active Systemwide and project specific activities + def active_activities + overridden_activity_ids = self.time_entry_activities.collect(&:parent_id) + + if overridden_activity_ids.empty? + return TimeEntryActivity.shared.active + else + return system_activities_and_project_overrides + end + end + + # Returns all the Systemwide and project specific activities + # (inactive and active) + def all_activities + overridden_activity_ids = self.time_entry_activities.collect(&:parent_id) + + if overridden_activity_ids.empty? + return TimeEntryActivity.shared + else + return system_activities_and_project_overrides(true) + end + end + + # Returns the systemwide active activities merged with the project specific overrides + def system_activities_and_project_overrides(include_inactive=false) + if include_inactive + return TimeEntryActivity.shared. + where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all + + self.time_entry_activities + else + return TimeEntryActivity.shared.active. + where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all + + self.time_entry_activities.active + end + end + + # Archives subprojects recursively + def archive! + children.each do |subproject| + subproject.send :archive! + end + update_attribute :status, STATUS_ARCHIVED + end + + def update_position_under_parent + set_or_update_position_under(parent) + end + + # Inserts/moves the project so that target's children or root projects stay alphabetically sorted + def set_or_update_position_under(target_parent) + parent_was = parent + sibs = (target_parent.nil? ? self.class.roots : target_parent.children) + to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase } + + if to_be_inserted_before + move_to_left_of(to_be_inserted_before) + elsif target_parent.nil? + if sibs.empty? + # move_to_root adds the project in first (ie. left) position + move_to_root + else + move_to_right_of(sibs.last) unless self == sibs.last + end + else + # move_to_child_of adds the project in last (ie.right) position + move_to_child_of(target_parent) + end + if parent_was != target_parent + after_parent_changed(parent_was) + end + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/04/0490719dc3185896fb07e210967a75c0f5e40a0a.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/04/0490719dc3185896fb07e210967a75c0f5e40a0a.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,54 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 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 File.expand_path('../../test_helper', __FILE__) + +class IssueTransactionTest < ActiveSupport::TestCase + fixtures :projects, :users, :members, :member_roles, :roles, + :trackers, :projects_trackers, + :versions, + :issue_statuses, :issue_categories, :issue_relations, :workflows, + :enumerations, + :issues, + :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values, + :time_entries + + self.use_transactional_fixtures = false + + def test_invalid_move_to_another_project + parent1 = Issue.generate! + child = Issue.generate!(:parent_issue_id => parent1.id) + grandchild = Issue.generate!(:parent_issue_id => child.id, :tracker_id => 2) + Project.find(2).tracker_ids = [1] + + parent1.reload + assert_equal [1, parent1.id, 1, 6], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt] + + # child can not be moved to Project 2 because its child is on a disabled tracker + child = Issue.find(child.id) + child.project = Project.find(2) + assert !child.save + child.reload + grandchild.reload + parent1.reload + + # no change + assert_equal [1, parent1.id, 1, 6], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt] + assert_equal [1, parent1.id, 2, 5], [child.project_id, child.root_id, child.lft, child.rgt] + assert_equal [1, parent1.id, 3, 4], [grandchild.project_id, grandchild.root_id, grandchild.lft, grandchild.rgt] + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/04/049234b460fae5eb30eba8a2487d0eac09c6701f.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/04/049234b460fae5eb30eba8a2487d0eac09c6701f.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,174 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 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. + +namespace :redmine do + namespace :email do + + desc <<-END_DESC +Read an email from standard input. + +General options: + unknown_user=ACTION how to handle emails from an unknown user + ACTION can be one of the following values: + ignore: email is ignored (default) + accept: accept as anonymous user + create: create a user account + no_permission_check=1 disable permission checking when receiving + the email + no_account_notice=1 disable new user account notification + default_group=foo,bar adds created user to foo and bar groups + +Issue attributes control options: + project=PROJECT identifier of the target project + status=STATUS name of the target status + tracker=TRACKER name of the target tracker + category=CATEGORY name of the target category + priority=PRIORITY name of the target priority + allow_override=ATTRS allow email content to override attributes + specified by previous options + ATTRS is a comma separated list of attributes + +Examples: + # No project specified. Emails MUST contain the 'Project' keyword: + rake redmine:email:read RAILS_ENV="production" < raw_email + + # Fixed project and default tracker specified, but emails can override + # both tracker and priority attributes: + rake redmine:email:read RAILS_ENV="production" \\ + project=foo \\ + tracker=bug \\ + allow_override=tracker,priority < raw_email +END_DESC + + task :read => :environment do + MailHandler.receive(STDIN.read, MailHandler.extract_options_from_env(ENV)) + end + + desc <<-END_DESC +Read emails from an IMAP server. + +General options: + unknown_user=ACTION how to handle emails from an unknown user + ACTION can be one of the following values: + ignore: email is ignored (default) + accept: accept as anonymous user + create: create a user account + no_permission_check=1 disable permission checking when receiving + the email + no_account_notice=1 disable new user account notification + default_group=foo,bar adds created user to foo and bar groups + +Available IMAP options: + host=HOST IMAP server host (default: 127.0.0.1) + port=PORT IMAP server port (default: 143) + ssl=SSL Use SSL? (default: false) + username=USERNAME IMAP account + password=PASSWORD IMAP password + folder=FOLDER IMAP folder to read (default: INBOX) + +Issue attributes control options: + project=PROJECT identifier of the target project + status=STATUS name of the target status + tracker=TRACKER name of the target tracker + category=CATEGORY name of the target category + priority=PRIORITY name of the target priority + 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: + + rake redmine:email:receive_imap RAILS_ENV="production" \\ + host=imap.foo.bar username=redmine@example.net password=xxx + + + # Fixed project and default tracker specified, but emails can override + # both tracker and priority attributes: + + rake redmine:email:receive_imap RAILS_ENV="production" \\ + host=imap.foo.bar username=redmine@example.net password=xxx ssl=1 \\ + project=foo \\ + tracker=bug \\ + allow_override=tracker,priority +END_DESC + + task :receive_imap => :environment do + imap_options = {:host => ENV['host'], + :port => ENV['port'], + :ssl => ENV['ssl'], + :username => ENV['username'], + :password => ENV['password'], + :folder => ENV['folder'], + :move_on_success => ENV['move_on_success'], + :move_on_failure => ENV['move_on_failure']} + + Redmine::IMAP.check(imap_options, MailHandler.extract_options_from_env(ENV)) + end + + desc <<-END_DESC +Read emails from an POP3 server. + +Available POP3 options: + host=HOST POP3 server host (default: 127.0.0.1) + port=PORT POP3 server port (default: 110) + username=USERNAME POP3 account + password=PASSWORD POP3 password + apop=1 use APOP authentication (default: false) + delete_unprocessed=1 delete messages that could not be processed + successfully from the server (default + behaviour is to leave them on the server) + +See redmine:email:receive_imap for more options and examples. +END_DESC + + task :receive_pop3 => :environment do + pop_options = {:host => ENV['host'], + :port => ENV['port'], + :apop => ENV['apop'], + :username => ENV['username'], + :password => ENV['password'], + :delete_unprocessed => ENV['delete_unprocessed']} + + Redmine::POP3.check(pop_options, MailHandler.extract_options_from_env(ENV)) + end + + desc "Send a test email to the user with the provided login name" + task :test, [:login] => :environment do |task, args| + include Redmine::I18n + abort l(:notice_email_error, "Please include the user login to test with. Example: rake redmine:email:test[login]") if args[:login].blank? + + user = User.find_by_login(args[:login]) + abort l(:notice_email_error, "User #{args[:login]} not found") unless user && user.logged? + + ActionMailer::Base.raise_delivery_errors = true + begin + Mailer.with_synched_deliveries do + Mailer.test_email(user).deliver + end + puts l(:notice_email_sent, user.mail) + rescue Exception => e + abort l(:notice_email_error, e.message) + end + end + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/04/04dcbe1c1303811a184d8f2171e27c9cdde0c767.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/04/04dcbe1c1303811a184d8f2171e27c9cdde0c767.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,1091 @@ +fa: + # Text direction: Left-to-Right (ltr) or Right-to-Left (rtl) + direction: rtl + date: + formats: + # Use the strftime parameters for formats. + # When no format has been given, it uses default. + # You can provide other formats here if you like! + default: "%Y/%m/%d" + short: "%b %d" + long: "%B %d, %Y" + + day_names: [یک‌شنبه, دوشنبه, سه‌شنبه, چهارشنبه, پنج‌شنبه, آدینه, شنبه] + abbr_day_names: [یک, دو, سه, چهار, پنج, آدینه, شنبه] + + # Don't forget the nil at the beginning; there's no such thing as a 0th month + month_names: [~, ژانویه, Ùوریه, مارس, آوریل, مه, ژوئن, ژوئیه, اوت, سپتامبر, اکتبر, نوامبر, دسامبر] + abbr_month_names: [~, ژان, Ùور, مار, آور, مه, ژوئن, ژوئیه, اوت, سپت, اکت, نوا, دسا] + # Used in date_select and datime_select. + order: + - :year + - :month + - :day + + time: + formats: + default: "%Y/%m/%d - %H:%M" + time: "%H:%M" + short: "%d %b %H:%M" + long: "%d %B %Y ساعت %H:%M" + am: "صبØ" + pm: "عصر" + + datetime: + distance_in_words: + half_a_minute: "نیم دقیقه" + less_than_x_seconds: + one: "کمتر از 1 ثانیه" + other: "کمتر از %{count} ثانیه" + x_seconds: + one: "1 ثانیه" + other: "%{count} ثانیه" + less_than_x_minutes: + one: "کمتر از 1 دقیقه" + other: "کمتر از %{count} دقیقه" + x_minutes: + one: "1 دقیقه" + other: "%{count} دقیقه" + about_x_hours: + one: "نزدیک 1 ساعت" + other: "نزدیک %{count} ساعت" + x_hours: + one: "1 ساعت" + other: "%{count} ساعت" + x_days: + one: "1 روز" + other: "%{count} روز" + about_x_months: + one: "نزدیک 1 ماه" + other: "نزدیک %{count} ماه" + x_months: + one: "1 ماه" + other: "%{count} ماه" + about_x_years: + one: "نزدیک 1 سال" + other: "نزدیک %{count} سال" + over_x_years: + one: "بیش از 1 سال" + other: "بیش از %{count} سال" + almost_x_years: + one: "نزدیک 1 سال" + other: "نزدیک %{count} سال" + + number: + # Default format for numbers + format: + separator: "Ù«" + delimiter: "" + precision: 3 + human: + format: + delimiter: "" + precision: 3 + storage_units: + format: "%n %u" + units: + byte: + one: "بایت" + other: "بایت" + kb: "کیلوبایت" + mb: "مگابایت" + gb: "گیگابایت" + tb: "ترابایت" + +# Used in array.to_sentence. + support: + array: + sentence_connector: "Ùˆ" + skip_last_comma: false + + activerecord: + errors: + template: + header: + one: "1 ایراد از ذخیره سازی این %{model} جلوگیری کرد" + other: "%{count} ایراد از ذخیره سازی این %{model} جلوگیری کرد" + messages: + inclusion: "در Ùهرست نیامده است" + exclusion: "رزرو شده است" + invalid: "نادرست است" + confirmation: "با بررسی سازگاری ندارد" + accepted: "باید Ù¾Ø°ÛŒØ±ÙØªÙ‡ شود" + empty: "نمی‌تواند تهی باشد" + blank: "نمی‌تواند تهی باشد" + too_long: "خیلی بلند است (بیشترین اندازه %{count} نویسه است)" + too_short: "خیلی کوتاه است (کمترین اندازه %{count} نویسه است)" + wrong_length: "اندازه نادرست است (باید %{count} نویسه باشد)" + taken: "پیش از این Ú¯Ø±ÙØªÙ‡ شده است" + not_a_number: "شماره درستی نیست" + not_a_date: "تاریخ درستی نیست" + greater_than: "باید بزرگتر از %{count} باشد" + greater_than_or_equal_to: "باید بزرگتر از یا برابر با %{count} باشد" + equal_to: "باید برابر با %{count} باشد" + less_than: "باید کمتر از %{count} باشد" + less_than_or_equal_to: "باید کمتر از یا برابر با %{count} باشد" + odd: "باید ÙØ±Ø¯ باشد" + even: "باید زوج باشد" + greater_than_start_date: "باید از تاریخ آغاز بزرگتر باشد" + not_same_project: "به همان پروژه وابسته نیست" + circular_dependency: "این وابستگی یک وابستگی دایره وار خواهد ساخت" + cant_link_an_issue_with_a_descendant: "یک پیامد نمی‌تواند به یکی از زیر کارهایش پیوند بخورد" + + actionview_instancetag_blank_option: گزینش کنید + + general_text_No: 'خیر' + general_text_Yes: 'آری' + general_text_no: 'خیر' + general_text_yes: 'آری' + general_lang_name: 'Persian (پارسی)' + general_csv_separator: ',' + general_csv_decimal_separator: '.' + general_csv_encoding: UTF-8 + general_pdf_encoding: UTF-8 + general_first_day_of_week: '6' + + notice_account_updated: ØØ³Ø§Ø¨ شما بروز شد. + notice_account_invalid_creditentials: نام کاربری یا گذرواژه نادرست است + notice_account_password_updated: گذرواژه بروز شد + notice_account_wrong_password: گذرواژه نادرست است + notice_account_register_done: ØØ³Ø§Ø¨ ساخته شد. برای ÙØ¹Ø§Ù„ نمودن آن، روی پیوندی Ú©Ù‡ به شما ایمیل شده کلیک کنید. + notice_account_unknown_email: کاربر شناخته نشد. + notice_can_t_change_password: این ØØ³Ø§Ø¨ یک روش شناسایی بیرونی را به کار Ú¯Ø±ÙØªÙ‡ است. گذرواژه را نمی‌توان جایگزین کرد. + notice_account_lost_email_sent: یک ایمیل با راهنمایی درباره گزینش گذرواژه تازه برای شما ÙØ±Ø³ØªØ§Ø¯Ù‡ شد. + notice_account_activated: ØØ³Ø§Ø¨ شما ÙØ¹Ø§Ù„ شده است. اکنون می‌توانید وارد شوید. + notice_successful_create: با موÙقیت ساخته شد. + notice_successful_update: با موÙقیت بروز شد. + notice_successful_delete: با موÙقیت برداشته شد. + notice_successful_connection: با موÙقیت متصل شد. + notice_file_not_found: برگه درخواستی شما در دسترس نیست یا پاک شده است. + notice_locking_conflict: داده‌ها را کاربر دیگری بروز کرده است. + notice_not_authorized: شما به این برگه دسترسی ندارید. + notice_not_authorized_archived_project: پروژه درخواستی شما بایگانی شده است. + notice_email_sent: "یک ایمیل به %{value} ÙØ±Ø³ØªØ§Ø¯Ù‡ شد." + notice_email_error: "یک ایراد در ÙØ±Ø³ØªØ§Ø¯Ù† ایمیل پیش آمد (%{value})." + notice_feeds_access_key_reseted: کلید دسترسی RSS شما بازنشانی شد. + notice_api_access_key_reseted: کلید دسترسی API شما بازنشانی شد. + notice_failed_to_save_issues: "ذخیره سازی %{count} پیامد از %{total} پیامد گزینش شده شکست خورد: %{ids}." + notice_failed_to_save_members: "ذخیره سازی اعضا شکست خورد: %{errors}." + notice_no_issue_selected: "هیچ پیامدی برگزیده نشده است! پیامدهایی Ú©Ù‡ می‌خواهید ویرایش کنید را برگزینید." + notice_account_pending: "ØØ³Ø§Ø¨ شما ساخته شد Ùˆ اکنون چشم به راه روادید سرپرست است." + notice_default_data_loaded: پیکربندی پیش‌گزیده با موÙقیت بار شد. + notice_unable_delete_version: نگارش را نمی‌توان پاک کرد. + notice_unable_delete_time_entry: زمان گزارش شده را نمی‌توان پاک کرد. + notice_issue_done_ratios_updated: اندازه انجام شده پیامد بروز شد. + notice_gantt_chart_truncated: "نمودار بریده شد چون از بیشترین شماری Ú©Ù‡ می‌توان نشان داد بزگتر است (%{max})." + + error_can_t_load_default_data: "پیکربندی پیش‌گزیده نمی‌تواند بار شود: %{value}" + error_scm_not_found: "بخش یا نگارش در انباره پیدا نشد." + error_scm_command_failed: "ایرادی در دسترسی به انباره پیش آمد: %{value}" + error_scm_annotate: "بخش پیدا نشد یا نمی‌توان برای آن یادداشت نوشت." + error_issue_not_found_in_project: 'پیامد پیدا نشد یا به این پروژه وابسته نیست.' + error_no_tracker_in_project: 'هیچ پیگردی به این پروژه پیوسته نشده است. پیکربندی پروژه را بررسی کنید.' + error_no_default_issue_status: 'هیچ وضعیت پیامد پیش‌گزیده‌ای مشخص نشده است. پیکربندی را بررسی کنید (به «پیکربندی -> وضعیت‌های پیامد» بروید).' + error_can_not_delete_custom_field: Ùیلد Ø³ÙØ§Ø±Ø´ÛŒ را نمی‌توان پاک کرد. + error_can_not_delete_tracker: "این پیگرد دارای پیامد است Ùˆ نمی‌توان آن را پاک کرد." + error_can_not_remove_role: "این نقش به کار Ú¯Ø±ÙØªÙ‡ شده است Ùˆ نمی‌توان آن را پاک کرد." + error_can_not_reopen_issue_on_closed_version: 'یک پیامد Ú©Ù‡ به یک نگارش بسته شده وابسته است را نمی‌توان باز کرد.' + error_can_not_archive_project: این پروژه را نمی‌توان بایگانی کرد. + error_issue_done_ratios_not_updated: "اندازه انجام شده پیامد بروز نشد." + error_workflow_copy_source: 'یک پیگرد یا نقش منبع را برگزینید.' + error_workflow_copy_target: 'پیگردها یا نقش‌های مقصد را برگزینید.' + error_unable_delete_issue_status: 'وضعیت پیامد را نمی‌توان پاک کرد.' + error_unable_to_connect: "نمی‌توان متصل شد (%{value})" + warning_attachments_not_saved: "%{count} پرونده ذخیره نشد." + + mail_subject_lost_password: "گذرواژه ØØ³Ø§Ø¨ %{value} شما" + mail_body_lost_password: 'برای جایگزینی گذرواژه خود، بر روی پیوند زیر کلیک کنید:' + mail_subject_register: "ÙØ¹Ø§Ù„سازی ØØ³Ø§Ø¨ %{value} شما" + mail_body_register: 'برای ÙØ¹Ø§Ù„سازی ØØ³Ø§Ø¨ خود، بر روی پیوند زیر کلیک کنید:' + mail_body_account_information_external: "شما می‌توانید ØØ³Ø§Ø¨ %{value} خود را برای ورود به کار برید." + mail_body_account_information: داده‌های ØØ³Ø§Ø¨ شما + mail_subject_account_activation_request: "درخواست ÙØ¹Ø§Ù„سازی ØØ³Ø§Ø¨ %{value}" + mail_body_account_activation_request: "یک کاربر تازه (%{value}) نامنویسی کرده است. این ØØ³Ø§Ø¨ چشم به راه روادید شماست:" + mail_subject_reminder: "زمان رسیدگی به %{count} پیامد در %{days} روز آینده سر می‌رسد" + mail_body_reminder: "زمان رسیدگی به %{count} پیامد Ú©Ù‡ به شما واگذار شده است، در %{days} روز آینده سر می‌رسد:" + mail_subject_wiki_content_added: "برگه ویکی «%{id}» Ø§ÙØ²ÙˆØ¯Ù‡ شد" + mail_body_wiki_content_added: "برگه ویکی «%{id}» به دست %{author} Ø§ÙØ²ÙˆØ¯Ù‡ شد." + mail_subject_wiki_content_updated: "برگه ویکی «%{id}» بروز شد" + mail_body_wiki_content_updated: "برگه ویکی «%{id}» به دست %{author} بروز شد." + + + field_name: نام + field_description: یادداشت + field_summary: خلاصه + field_is_required: الزامی + field_firstname: نام Ú©ÙˆÚ†Ú© + field_lastname: نام خانوادگی + field_mail: ایمیل + field_filename: پرونده + field_filesize: اندازه + field_downloads: Ø¯Ø±ÛŒØ§ÙØªâ€ŒÙ‡Ø§ + field_author: نویسنده + field_created_on: ساخته شده در + field_updated_on: بروز شده در + field_field_format: قالب + field_is_for_all: برای همه پروژه‌ها + field_possible_values: مقادیر ممکن + field_regexp: عبارت منظم + field_min_length: کمترین اندازه + field_max_length: بیشترین اندازه + field_value: مقدار + field_category: دسته + field_title: عنوان + field_project: پروژه + field_issue: پیامد + field_status: وضعیت + field_notes: یادداشت + field_is_closed: پیامد بسته شده + field_is_default: مقدار پیش‌گزیده + field_tracker: پیگرد + field_subject: موضوع + field_due_date: زمان سررسید + field_assigned_to: واگذار شده به + field_priority: برتری + field_fixed_version: نگارش هد٠+ field_user: کاربر + field_principal: دستور دهنده + field_role: نقش + field_homepage: برگه خانه + field_is_public: همگانی + field_parent: پروژه پدر + field_is_in_roadmap: این پیامدها در چشم‌انداز نشان داده شوند + field_login: ورود + field_mail_notification: آگاه سازی‌های ایمیلی + field_admin: سرپرست + field_last_login_on: آخرین ورود + field_language: زبان + field_effective_date: تاریخ + field_password: گذرواژه + field_new_password: گذرواژه تازه + field_password_confirmation: بررسی گذرواژه + field_version: نگارش + field_type: گونه + field_host: میزبان + field_port: درگاه + field_account: ØØ³Ø§Ø¨ + field_base_dn: DN پایه + field_attr_login: نشانه ورود + field_attr_firstname: نشانه نام Ú©ÙˆÚ†Ú© + field_attr_lastname: نشانه نام خانوادگی + field_attr_mail: نشانه ایمیل + field_onthefly: ساخت کاربر بیدرنگ + field_start_date: تاریخ آغاز + field_done_ratio: Ùª انجام شده + field_auth_source: روش شناسایی + field_hide_mail: ایمیل من پنهان شود + field_comments: دیدگاه + field_url: نشانی + field_start_page: برگه آغاز + field_subproject: زیر پروژه + field_hours: ساعت‌ + field_activity: گزارش + field_spent_on: در تاریخ + field_identifier: شناسه + field_is_filter: پالایش پذیر + field_issue_to: پیامد وابسته + field_delay: دیرکرد + field_assignable: پیامدها می‌توانند به این نقش واگذار شوند + field_redirect_existing_links: پیوندهای پیشین به پیوند تازه راهنمایی شوند + field_estimated_hours: زمان برآورد شده + field_column_names: ستون‌ها + field_time_entries: زمان نوشتن + field_time_zone: پهنه زمانی + field_searchable: جستجو پذیر + field_default_value: مقدار پیش‌گزیده + field_comments_sorting: نمایش دیدگاه‌ها + field_parent_title: برگه پدر + field_editable: ویرایش پذیر + field_watcher: دیده‌بان + field_identity_url: نشانی OpenID + field_content: Ù…ØØªÙˆØ§ + field_group_by: دسته بندی با + field_sharing: اشتراک گذاری + field_parent_issue: کار پدر + field_member_of_group: "دسته واگذار شونده" + field_assigned_to_role: "نقش واگذار شونده" + field_text: Ùیلد متنی + field_visible: آشکار + + setting_app_title: نام برنامه + setting_app_subtitle: زیرنام برنامه + setting_welcome_text: نوشتار خوش‌آمد گویی + setting_default_language: زبان پیش‌گزیده + setting_login_required: الزامی بودن ورود + setting_self_registration: خود نام نویسی + setting_attachment_max_size: بیشترین اندازه پیوست + setting_issues_export_limit: کرانه صدور پییامدها + setting_mail_from: نشانی ÙØ±Ø³ØªÙ†Ø¯Ù‡ ایمیل + setting_bcc_recipients: گیرندگان ایمیل دیده نشوند (bcc) + setting_plain_text_mail: ایمیل نوشته ساده (بدون HTML) + setting_host_name: نام میزبان Ùˆ نشانی + setting_text_formatting: قالب بندی نوشته + setting_wiki_compression: ÙØ´Ø±Ø¯Ù‡â€ŒØ³Ø§Ø²ÛŒ پیشینه ویکی + setting_feeds_limit: کرانه Ù…ØØªÙˆØ§ÛŒ خوراک + setting_default_projects_public: ØØ§Ù„ت پیش‌گزیده پروژه‌های تازه، همگانی است + setting_autofetch_changesets: Ø¯Ø±ÛŒØ§ÙØª خودکار تغییرات + setting_sys_api_enabled: ÙØ¹Ø§Ù„ سازی وب سرویس برای سرپرستی انباره + setting_commit_ref_keywords: کلیدواژه‌های نشانه + setting_commit_fix_keywords: کلیدواژه‌های انجام + setting_autologin: ورود خودکار + setting_date_format: قالب تاریخ + setting_time_format: قالب زمان + setting_cross_project_issue_relations: توانایی وابستگی میان پروژه‌ای پیامدها + setting_issue_list_default_columns: ستون‌های پیش‌گزیده نمایش داده شده در Ùهرست پیامدها + setting_emails_header: سرنویس ایمیل‌ها + setting_emails_footer: پانویس ایمیل‌ها + setting_protocol: پیوندنامه + setting_per_page_options: گزینه‌های اندازه داده‌های هر برگ + setting_user_format: قالب نمایشی کاربران + setting_activity_days_default: روزهای نمایش داده شده در گزارش پروژه + setting_display_subprojects_issues: پیش‌گزیده نمایش پیامدهای زیرپروژه در پروژه پدر + setting_enabled_scm: ÙØ¹Ø§Ù„سازی SCM + setting_mail_handler_body_delimiters: "بریدن ایمیل‌ها پس از یکی از این ردیÙ‌ها" + setting_mail_handler_api_enabled: ÙØ¹Ø§Ù„سازی وب سرویس برای ایمیل‌های آمده + setting_mail_handler_api_key: کلید API + setting_sequential_project_identifiers: ساخت پشت سر هم شناسه پروژه + setting_gravatar_enabled: کاربرد Gravatar برای عکس کاربر + setting_gravatar_default: عکس Gravatar پیش‌گزیده + setting_diff_max_lines_displayed: بیشترین اندازه ردیÙ‌های ØªÙØ§ÙˆØª نشان داده شده + setting_file_max_size_displayed: بیشترین اندازه پرونده‌های نمایش داده شده درون خطی + setting_repository_log_display_limit: بیشترین شمار نگارش‌های نمایش داده شده در گزارش پرونده + setting_openid: پذیرش ورود Ùˆ نام نویسی با OpenID + setting_password_min_length: کمترین اندازه گذرواژه + setting_new_project_user_role_id: نقش داده شده به کاربری Ú©Ù‡ سرپرست نیست Ùˆ پروژه می‌سازد + setting_default_projects_modules: پیمانه‌های پیش‌گزیده ÙØ¹Ø§Ù„ برای پروژه‌های تازه + setting_issue_done_ratio: برآورد اندازه انجام شده پیامد با + setting_issue_done_ratio_issue_field: کاربرد Ùیلد پیامد + setting_issue_done_ratio_issue_status: کاربرد وضعیت پیامد + setting_start_of_week: آغاز گاهشمار از + setting_rest_api_enabled: ÙØ¹Ø§Ù„سازی وب سرویس‌های REST + setting_cache_formatted_text: نهان سازی نوشته‌های قالب بندی شده + setting_default_notification_option: آگاه سازی پیش‌گزیده + setting_commit_logtime_enabled: ÙØ¹Ø§Ù„سازی زمان گذاشته شده + setting_commit_logtime_activity_id: کار زمان گذاشته شده + setting_gantt_items_limit: بیشترین شمار بخش‌های نمایش داده شده در نمودار گانت + + permission_add_project: ساخت پروژه + permission_add_subprojects: ساخت زیرپروژه + permission_edit_project: ویرایش پروژه + permission_select_project_modules: گزینش پیمانه‌های پروژه + permission_manage_members: سرپرستی اعضا + permission_manage_project_activities: سرپرستی کارهای پروژه + permission_manage_versions: سرپرستی نگارش‌ها + permission_manage_categories: سرپرستی دسته‌های پیامد + permission_view_issues: دیدن پیامدها + permission_add_issues: Ø§ÙØ²ÙˆØ¯Ù† پیامدها + permission_edit_issues: ویرایش پیامدها + permission_manage_issue_relations: سرپرستی وابستگی پیامدها + permission_add_issue_notes: Ø§ÙØ²ÙˆØ¯Ù† یادداشت + permission_edit_issue_notes: ویرایش یادداشت + permission_edit_own_issue_notes: ویرایش یادداشت خود + permission_move_issues: جابجایی پیامدها + permission_delete_issues: پاک کردن پیامدها + permission_manage_public_queries: سرپرستی پرس‌وجوهای همگانی + permission_save_queries: ذخیره سازی پرس‌وجوها + permission_view_gantt: دیدن نمودار گانت + permission_view_calendar: دیدن گاهشمار + permission_view_issue_watchers: دیدن Ùهرست دیده‌بان‌ها + permission_add_issue_watchers: Ø§ÙØ²ÙˆØ¯Ù† دیده‌بان‌ها + permission_delete_issue_watchers: پاک کردن دیده‌بان‌ها + permission_log_time: نوشتن زمان گذاشته شده + permission_view_time_entries: دیدن زمان گذاشته شده + permission_edit_time_entries: ویرایش زمان گذاشته شده + permission_edit_own_time_entries: ویرایش زمان گذاشته شده خود + permission_manage_news: سرپرستی رویدادها + permission_comment_news: گذاشتن دیدگاه روی رویدادها + permission_view_documents: دیدن نوشتارها + permission_manage_files: سرپرستی پرونده‌ها + permission_view_files: دیدن پرونده‌ها + permission_manage_wiki: سرپرستی ویکی + permission_rename_wiki_pages: نامگذاری برگه ویکی + permission_delete_wiki_pages: پاک کردن برگه ویکی + permission_view_wiki_pages: دیدن ویکی + permission_view_wiki_edits: دیدن پیشینه ویکی + permission_edit_wiki_pages: ویرایش برگه‌های ویکی + permission_delete_wiki_pages_attachments: پاک کردن پیوست‌های برگه ویکی + permission_protect_wiki_pages: نگه‌داری برگه‌های ویکی + permission_manage_repository: سرپرستی انباره + permission_browse_repository: چریدن در انباره + permission_view_changesets: دیدن تغییرات + permission_commit_access: دسترسی تغییر انباره + permission_manage_boards: سرپرستی انجمن‌ها + permission_view_messages: دیدن پیام‌ها + permission_add_messages: ÙØ±Ø³ØªØ§Ø¯Ù† پیام‌ها + permission_edit_messages: ویرایش پیام‌ها + permission_edit_own_messages: ویرایش پیام خود + permission_delete_messages: پاک کردن پیام‌ها + permission_delete_own_messages: پاک کردن پیام خود + permission_export_wiki_pages: صدور برگه‌های ویکی + permission_manage_subtasks: سرپرستی زیرکارها + + project_module_issue_tracking: پیگیری پیامدها + project_module_time_tracking: پیگیری زمان + project_module_news: رویدادها + project_module_documents: نوشتارها + project_module_files: پرونده‌ها + project_module_wiki: ویکی + project_module_repository: انباره + project_module_boards: انجمن‌ها + project_module_calendar: گاهشمار + project_module_gantt: گانت + + label_user: کاربر + label_user_plural: کاربر + label_user_new: کاربر تازه + label_user_anonymous: ناشناس + label_project: پروژه + label_project_new: پروژه تازه + label_project_plural: پروژه + label_x_projects: + zero: بدون پروژه + one: "1 پروژه" + other: "%{count} پروژه" + label_project_all: همه پروژه‌ها + label_project_latest: آخرین پروژه‌ها + label_issue: پیامد + label_issue_new: پیامد تازه + label_issue_plural: پیامد + label_issue_view_all: دیدن همه پیامدها + label_issues_by: "دسته‌بندی پیامدها با %{value}" + label_issue_added: پیامد Ø§ÙØ²ÙˆØ¯Ù‡ شد + label_issue_updated: پیامد بروز شد + label_document: نوشتار + label_document_new: نوشتار تازه + label_document_plural: نوشتار + label_document_added: نوشتار Ø§ÙØ²ÙˆØ¯Ù‡ شد + label_role: نقش + label_role_plural: نقش + label_role_new: نقش تازه + label_role_and_permissions: نقش‌ها Ùˆ پروانه‌ها + label_member: عضو + label_member_new: عضو تازه + label_member_plural: عضو + label_tracker: پیگرد + label_tracker_plural: پیگرد + label_tracker_new: پیگرد تازه + label_workflow: گردش کار + label_issue_status: وضعیت پیامد + label_issue_status_plural: وضعیت پیامد + label_issue_status_new: وضعیت تازه + label_issue_category: دسته پیامد + label_issue_category_plural: دسته پیامد + label_issue_category_new: دسته تازه + label_custom_field: Ùیلد Ø³ÙØ§Ø±Ø´ÛŒ + label_custom_field_plural: Ùیلد Ø³ÙØ§Ø±Ø´ÛŒ + label_custom_field_new: Ùیلد Ø³ÙØ§Ø±Ø´ÛŒ تازه + label_enumerations: برشمردنی‌ها + label_enumeration_new: مقدار تازه + label_information: داده + label_information_plural: داده + label_please_login: وارد شوید + label_register: نام نویسی کنید + label_login_with_open_id_option: یا با OpenID وارد شوید + label_password_lost: Ø¨Ø§Ø²ÛŒØ§ÙØª گذرواژه + label_home: سرآغاز + label_my_page: برگه من + label_my_account: ØØ³Ø§Ø¨ من + label_my_projects: پروژه‌های من + label_my_page_block: بخش برگه من + label_administration: سرپرستی + label_login: ورود + label_logout: خروج + label_help: راهنما + label_reported_issues: پیامدهای گزارش شده + label_assigned_to_me_issues: پیامدهای واگذار شده به من + label_last_login: آخرین ورود + label_registered_on: نام نویسی شده در + label_activity: گزارش + label_overall_activity: گزارش روی هم Ø±ÙØªÙ‡ + label_user_activity: "گزارش %{value}" + label_new: تازه + label_logged_as: "نام کاربری:" + label_environment: Ù…ØÛŒØ· + label_authentication: شناسایی + label_auth_source: روش شناسایی + label_auth_source_new: روش شناسایی تازه + label_auth_source_plural: روش شناسایی + label_subproject_plural: زیرپروژه + label_subproject_new: زیرپروژه تازه + label_and_its_subprojects: "%{value} Ùˆ زیرپروژه‌هایش" + label_min_max_length: کمترین Ùˆ بیشترین اندازه + label_list: Ùهرست + label_date: تاریخ + label_integer: شماره درست + label_float: شماره شناور + label_boolean: درست/نادرست + label_string: نوشته + label_text: نوشته بلند + label_attribute: نشانه + label_attribute_plural: نشانه + label_no_data: هیچ داده‌ای برای نمایش نیست + label_change_status: جایگزینی وضعیت + label_history: پیشینه + label_attachment: پرونده + label_attachment_new: پرونده تازه + label_attachment_delete: پاک کردن پرونده + label_attachment_plural: پرونده + label_file_added: پرونده Ø§ÙØ²ÙˆØ¯Ù‡ شد + label_report: گزارش + label_report_plural: گزارش + label_news: رویداد + label_news_new: Ø§ÙØ²ÙˆØ¯Ù† رویداد + label_news_plural: رویداد + label_news_latest: آخرین رویدادها + label_news_view_all: دیدن همه رویدادها + label_news_added: رویداد Ø§ÙØ²ÙˆØ¯Ù‡ شد + label_settings: پیکربندی + label_overview: در یک نگاه + label_version: نگارش + label_version_new: نگارش تازه + label_version_plural: نگارش + label_close_versions: بستن نگارش‌های انجام شده + label_confirmation: بررسی + label_export_to: 'قالب‌های دیگر:' + label_read: خواندن... + label_public_projects: پروژه‌های همگانی + label_open_issues: باز + label_open_issues_plural: باز + label_closed_issues: بسته + label_closed_issues_plural: بسته + label_x_open_issues_abbr_on_total: + zero: 0 باز از %{total} + one: 1 باز از %{total} + other: "%{count} باز از %{total}" + label_x_open_issues_abbr: + zero: 0 باز + one: 1 باز + other: "%{count} باز" + label_x_closed_issues_abbr: + zero: 0 بسته + one: 1 بسته + other: "%{count} بسته" + label_total: جمله + label_permissions: پروانه‌ها + label_current_status: وضعیت کنونی + label_new_statuses_allowed: وضعیت‌های Ù¾Ø°ÛŒØ±ÙØªÙ†ÛŒ تازه + label_all: همه + label_none: هیچ + label_nobody: هیچکس + label_next: پسین + label_previous: پیشین + label_used_by: به کار Ø±ÙØªÙ‡ در + label_details: ریزه‌کاری + label_add_note: Ø§ÙØ²ÙˆØ¯Ù† یادداشت + label_per_page: ردیÙ‌ها در هر برگه + label_calendar: گاهشمار + label_months_from: از ماه + label_gantt: گانت + label_internal: درونی + label_last_changes: "%{count} تغییر آخر" + label_change_view_all: دیدن همه تغییرات + label_personalize_page: Ø³ÙØ§Ø±Ø´ÛŒ نمودن این برگه + label_comment: دیدگاه + label_comment_plural: دیدگاه + label_x_comments: + zero: بدون دیدگاه + one: 1 دیدگاه + other: "%{count} دیدگاه" + label_comment_add: Ø§ÙØ²ÙˆØ¯Ù† دیدگاه + label_comment_added: دیدگاه Ø§ÙØ²ÙˆØ¯Ù‡ شد + label_comment_delete: پاک کردن دیدگاه‌ها + label_query: پرس‌وجوی Ø³ÙØ§Ø±Ø´ÛŒ + label_query_plural: پرس‌وجوی Ø³ÙØ§Ø±Ø´ÛŒ + label_query_new: پرس‌وجوی تازه + label_filter_add: Ø§ÙØ²ÙˆØ¯Ù† پالایه + label_filter_plural: پالایه + label_equals: برابر است با + label_not_equals: برابر نیست با + label_in_less_than: کمتر است از + label_in_more_than: بیشتر است از + label_greater_or_equal: بیشتر یا برابر است با + label_less_or_equal: کمتر یا برابر است با + label_in: در + label_today: امروز + label_all_time: همیشه + label_yesterday: دیروز + label_this_week: این Ù‡ÙØªÙ‡ + label_last_week: Ù‡ÙØªÙ‡ پیشین + label_last_n_days: "%{count} روز گذشته" + label_this_month: این ماه + label_last_month: ماه پیشین + label_this_year: امسال + label_date_range: بازه تاریخ + label_less_than_ago: کمتر از چند روز پیشین + label_more_than_ago: بیشتر از چند روز پیشین + label_ago: روز پیشین + label_contains: دارد + label_not_contains: ندارد + label_day_plural: روز + label_repository: انباره + label_repository_plural: انباره + label_browse: چریدن + label_branch: شاخه + label_tag: برچسب + label_revision: بازبینی + label_revision_plural: بازبینی + label_revision_id: "بازبینی %{value}" + label_associated_revisions: بازبینی‌های وابسته + label_added: Ø§ÙØ²ÙˆØ¯Ù‡ شده + label_modified: پیراسته شده + label_copied: رونویسی شده + label_renamed: نامگذاری شده + label_deleted: پاکسازی شده + label_latest_revision: آخرین بازبینی + label_latest_revision_plural: آخرین بازبینی + label_view_revisions: دیدن بازبینی‌ها + label_view_all_revisions: دیدن همه بازبینی‌ها + label_max_size: بیشترین اندازه + label_sort_highest: بردن به آغاز + label_sort_higher: بردن به بالا + label_sort_lower: بردن به پایین + label_sort_lowest: بردن به پایان + label_roadmap: چشم‌انداز + label_roadmap_due_in: "سررسید در %{value}" + label_roadmap_overdue: "%{value} دیرکرد" + label_roadmap_no_issues: هیچ پیامدی برای این نگارش نیست + label_search: جستجو + label_result_plural: دست‌آورد + label_all_words: همه واژه‌ها + label_wiki: ویکی + label_wiki_edit: ویرایش ویکی + label_wiki_edit_plural: ویرایش ویکی + label_wiki_page: برگه ویکی + label_wiki_page_plural: برگه ویکی + label_index_by_title: شاخص بر اساس نام + label_index_by_date: شاخص بر اساس تاریخ + label_current_version: نگارش کنونی + label_preview: پیش‌نمایش + label_feed_plural: خوراک + label_changes_details: ریز همه جایگذاری‌ها + label_issue_tracking: پیگیری پیامد + label_spent_time: زمان گذاشته شده + label_overall_spent_time: زمان گذاشته شده روی هم + label_f_hour: "%{value} ساعت" + label_f_hour_plural: "%{value} ساعت" + label_time_tracking: پیگیری زمان + label_change_plural: جایگذاری + label_statistics: سرشماری + label_commits_per_month: تغییر در هر ماه + label_commits_per_author: تغییر هر نویسنده + label_view_diff: دیدن ØªÙØ§ÙˆØªâ€ŒÙ‡Ø§ + label_diff_inline: همراستا + label_diff_side_by_side: کنار به کنار + label_options: گزینه‌ها + label_copy_workflow_from: رونویسی گردش کار از روی + label_permissions_report: گزارش پروانه‌ها + label_watched_issues: پیامدهای دیده‌بانی شده + label_related_issues: پیامدهای وابسته + label_applied_status: وضعیت به کار Ø±ÙØªÙ‡ + label_loading: بار گذاری... + label_relation_new: وابستگی تازه + label_relation_delete: پاک کردن وابستگی + label_relates_to: وابسته به + label_duplicates: نگارش دیگری از + label_duplicated_by: نگارشی دیگر در + label_blocks: بازداشت‌ها + label_blocked_by: بازداشت به دست + label_precedes: جلوتر است از + label_follows: پستر است از + label_end_to_start: پایان به آغاز + label_end_to_end: پایان به پایان + label_start_to_start: آغاز به آغاز + label_start_to_end: آغاز به پایان + label_stay_logged_in: وارد شده بمانید + label_disabled: ØºÛŒØ±ÙØ¹Ø§Ù„ + label_show_completed_versions: نمایش نگارش‌های انجام شده + label_me: من + label_board: انجمن + label_board_new: انجمن تازه + label_board_plural: انجمن + label_board_locked: Ù‚ÙÙ„ شده + label_board_sticky: چسبناک + label_topic_plural: Ø³Ø±ÙØµÙ„ + label_message_plural: پیام + label_message_last: آخرین پیام + label_message_new: پیام تازه + label_message_posted: پیام Ø§ÙØ²ÙˆØ¯Ù‡ شد + label_reply_plural: پاسخ + label_send_information: ÙØ±Ø³ØªØ§Ø¯Ù† داده‌های ØØ³Ø§Ø¨ به کاربر + label_year: سال + label_month: ماه + label_week: Ù‡ÙØªÙ‡ + label_date_from: از + label_date_to: تا + label_language_based: بر اساس زبان کاربر + label_sort_by: "جور کرد با %{value}" + label_send_test_email: ÙØ±Ø³ØªØ§Ø¯Ù† ایمیل آزمایشی + label_feeds_access_key: کلید دسترسی RSS + label_missing_feeds_access_key: کلید دسترسی RSS در دسترس نیست + label_feeds_access_key_created_on: "کلید دسترسی RSS %{value} پیش ساخته شده است" + label_module_plural: پیمانه + label_added_time_by: "Ø§ÙØ²ÙˆØ¯Ù‡ شده به دست %{author} در %{age} پیش" + label_updated_time_by: "بروز شده به دست %{author} در %{age} پیش" + label_updated_time: "بروز شده در %{value} پیش" + label_jump_to_a_project: پرش به یک پروژه... + label_file_plural: پرونده + label_changeset_plural: تغییر + label_default_columns: ستون‌های پیش‌گزیده + label_no_change_option: (بدون تغییر) + label_bulk_edit_selected_issues: ویرایش دسته‌ای پیامدهای گزینش شده + label_theme: پوسته + label_default: پیش‌گزیده + label_search_titles_only: تنها نام‌ها جستجو شود + label_user_mail_option_all: "برای هر رویداد در همه پروژه‌ها" + label_user_mail_option_selected: "برای هر رویداد تنها در پروژه‌های گزینش شده..." + label_user_mail_option_none: "هیچ رویدادی" + label_user_mail_option_only_my_events: "تنها برای چیزهایی Ú©Ù‡ دیده‌بان هستم یا در آن‌ها درگیر هستم" + label_user_mail_option_only_assigned: "تنها برای چیزهایی Ú©Ù‡ به من واگذار شده" + label_user_mail_option_only_owner: "تنها برای چیزهایی Ú©Ù‡ من دارنده آن‌ها هستم" + label_user_mail_no_self_notified: "نمی‌خواهم از تغییراتی Ú©Ù‡ خودم می‌دهم آگاه شوم" + label_registration_activation_by_email: ÙØ¹Ø§Ù„سازی ØØ³Ø§Ø¨ با ایمیل + label_registration_manual_activation: ÙØ¹Ø§Ù„سازی ØØ³Ø§Ø¨ دستی + label_registration_automatic_activation: ÙØ¹Ø§Ù„سازی ØØ³Ø§Ø¨ خودکار + label_display_per_page: "ردیÙ‌ها در هر برگه: %{value}" + label_age: سن + label_change_properties: ویرایش ویژگی‌ها + label_general: همگانی + label_more: بیشتر + label_scm: SCM + label_plugins: Ø§ÙØ²ÙˆÙ†Ù‡â€ŒÙ‡Ø§ + label_ldap_authentication: شناساییLDAP + label_downloads_abbr: Ø¯Ø±ÛŒØ§ÙØª + label_optional_description: یادداشت دلخواه + label_add_another_file: Ø§ÙØ²ÙˆØ¯Ù† پرونده دیگر + label_preferences: پسندها + label_chronological_order: به ترتیب تاریخ + label_reverse_chronological_order: برعکس ترتیب تاریخ + label_planning: برنامه ریزی + label_incoming_emails: ایمیل‌های آمده + label_generate_key: ساخت کلید + label_issue_watchers: دیده‌بان‌ها + label_example: نمونه + label_display: نمایش + label_sort: جور کرد + label_ascending: Ø§ÙØ²Ø§ÛŒØ´ÛŒ + label_descending: کاهشی + label_date_from_to: از %{start} تا %{end} + label_wiki_content_added: برگه ویکی Ø§ÙØ²ÙˆØ¯Ù‡ شد + label_wiki_content_updated: برگه ویکی بروز شد + label_group: دسته + label_group_plural: دسته + label_group_new: دسته تازه + label_time_entry_plural: زمان گذاشته شده + label_version_sharing_none: بدون اشتراک + label_version_sharing_descendants: با زیر پروژه‌ها + label_version_sharing_hierarchy: با رشته پروژه‌ها + label_version_sharing_tree: با درخت پروژه + label_version_sharing_system: با همه پروژه‌ها + label_update_issue_done_ratios: بروز رسانی اندازه انجام شده پیامد + label_copy_source: منبع + label_copy_target: مقصد + label_copy_same_as_target: مانند مقصد + label_display_used_statuses_only: تنها وضعیت‌هایی نشان داده شوند Ú©Ù‡ در این پیگرد به کار Ø±ÙØªÙ‡â€ŒØ§Ù†Ø¯ + label_api_access_key: کلید دسترسی API + label_missing_api_access_key: کلید دسترسی API در دسترس نیست + label_api_access_key_created_on: "کلید دسترسی API %{value} پیش ساخته شده است" + label_profile: نمایه + label_subtask_plural: زیرکار + label_project_copy_notifications: در هنگام رونویسی پروژه ایمیل‌های آگاه‌سازی را Ø¨ÙØ±Ø³Øª + label_principal_search: "جستجو برای کاربر یا دسته:" + label_user_search: "جستجو برای کاربر:" + + button_login: ورود + button_submit: واگذاری + button_save: نگهداری + button_check_all: گزینش همه + button_uncheck_all: گزینش هیچ + button_delete: پاک + button_create: ساخت + button_create_and_continue: ساخت Ùˆ ادامه + button_test: آزمایش + button_edit: ویرایش + button_edit_associated_wikipage: "ویرایش برگه ویکی وابسته: %{page_title}" + button_add: Ø§ÙØ²ÙˆØ¯Ù† + button_change: ویرایش + button_apply: انجام + button_clear: پاک + button_lock: گذاشتن Ù‚ÙÙ„ + button_unlock: برداشتن Ù‚ÙÙ„ + button_download: Ø¯Ø±ÛŒØ§ÙØª + button_list: Ùهرست + button_view: دیدن + button_move: جابجایی + button_move_and_follow: جابجایی Ùˆ ادامه + button_back: برگشت + button_cancel: بازگشت + button_activate: ÙØ¹Ø§Ù„سازی + button_sort: جور کرد + button_log_time: زمان‌نویسی + button_rollback: برگرد به این نگارش + button_watch: دیده‌بانی + button_unwatch: نا‌دیده‌بانی + button_reply: پاسخ + button_archive: بایگانی + button_unarchive: برگشت از بایگانی + button_reset: بازنشانی + button_rename: نامگذاری + button_change_password: جایگزینی گذرواژه + button_copy: رونوشت + button_copy_and_follow: رونوشت Ùˆ ادامه + button_annotate: یادداشت + button_update: بروز رسانی + button_configure: پیکربندی + button_quote: نقل قول + button_duplicate: نگارش دیگر + button_show: نمایش + + status_active: ÙØ¹Ø§Ù„ + status_registered: نام‌نویسی شده + status_locked: Ù‚ÙÙ„ + + version_status_open: باز + version_status_locked: Ù‚ÙÙ„ + version_status_closed: بسته + + field_active: ÙØ¹Ø§Ù„ + + text_select_mail_notifications: ÙØ±Ù…ان‌هایی Ú©Ù‡ برای آن‌ها باید ایمیل ÙØ±Ø³ØªØ§Ø¯Ù‡ شود را برگزینید. + text_regexp_info: برای نمونه ^[A-Z0-9]+$ + text_min_max_length_info: 0 یعنی بدون کران + text_project_destroy_confirmation: آیا براستی می‌خواهید این پروژه Ùˆ همه داده‌های آن را پاک کنید؟ + text_subprojects_destroy_warning: "زیرپروژه‌های آن: %{value} هم پاک خواهند شد." + text_workflow_edit: یک نقش Ùˆ یک پیگرد را برای ویرایش گردش کار برگزینید + text_are_you_sure: آیا این کار انجام شود؟ + text_journal_changed: "«%{label}» از «%{old}» به «%{new}» جایگزین شد" + text_journal_set_to: "«%{label}» به «%{value}» نشانده شد" + text_journal_deleted: "«%{label}» پاک شد (%{old})" + text_journal_added: "«%{label}»، «%{value}» را Ø§ÙØ²ÙˆØ¯" + text_tip_task_begin_day: روز آغاز پیامد + text_tip_task_end_day: روز پایان پیامد + text_tip_task_begin_end_day: روز آغاز Ùˆ پایان پیامد + text_caracters_maximum: "بیشترین اندازه %{count} است." + text_caracters_minimum: "کمترین اندازه %{count} است." + text_length_between: "باید میان %{min} Ùˆ %{max} نویسه باشد." + text_tracker_no_workflow: هیچ گردش کاری برای این پیگرد مشخص نشده است + text_unallowed_characters: نویسه‌های ناپسند + text_comma_separated: چند مقدار Ù¾Ø°ÛŒØ±ÙØªÙ†ÛŒ است (با «,» از هم جدا شوند). + text_line_separated: چند مقدار Ù¾Ø°ÛŒØ±ÙØªÙ†ÛŒ است (هر مقدار در یک خط). + text_issues_ref_in_commit_messages: نشانه روی Ùˆ بستن پیامدها در پیام‌های انباره + text_issue_added: "پیامد %{id} به دست %{author} گزارش شد." + text_issue_updated: "پیامد %{id} به دست %{author} بروز شد." + text_wiki_destroy_confirmation: آیا براستی می‌خواهید این ویکی Ùˆ همه Ù…ØØªÙˆØ§ÛŒ آن را پاک کنید؟ + text_issue_category_destroy_question: "برخی پیامدها (%{count}) به این دسته واگذار شده‌اند. می‌خواهید Ú†Ù‡ کنید؟" + text_issue_category_destroy_assignments: پاک کردن واگذاری به دسته + text_issue_category_reassign_to: واگذاری دوباره پیامدها به این دسته + text_user_mail_option: "برای پروژه‌های گزینش نشده، تنها ایمیل‌هایی درباره چیزهایی Ú©Ù‡ دیده‌بان یا درگیر آن‌ها هستید Ø¯Ø±ÛŒØ§ÙØª خواهید کرد (مانند پیامدهایی Ú©Ù‡ نویسنده آن‌ها هستید یا به شما واگذار شده‌اند)." + text_no_configuration_data: "نقش‌ها، پیگردها، وضعیت‌های پیامد Ùˆ گردش کار هنوز پیکربندی نشده‌اند. \nبه سختی پیشنهاد می‌شود Ú©Ù‡ پیکربندی پیش‌گزیده را بار کنید. سپس می‌توانید آن را ویرایش کنید." + text_load_default_configuration: بارگذاری پیکربندی پیش‌گزیده + text_status_changed_by_changeset: "در تغییر %{value} بروز شده است." + text_time_logged_by_changeset: "در تغییر %{value} نوشته شده است." + text_issues_destroy_confirmation: 'آیا براستی می‌خواهید پیامدهای گزینش شده را پاک کنید؟' + text_select_project_modules: 'پیمانه‌هایی Ú©Ù‡ باید برای این پروژه ÙØ¹Ø§Ù„ شوند را برگزینید:' + text_default_administrator_account_changed: ØØ³Ø§Ø¨ سرپرستی پیش‌گزیده جایگزین شد + text_file_repository_writable: پوشه پیوست‌ها نوشتنی است + text_plugin_assets_writable: پوشه دارایی‌های Ø§ÙØ²ÙˆÙ†Ù‡â€ŒÙ‡Ø§ نوشتنی است + text_rmagick_available: RMagick در دسترس است (اختیاری) + text_destroy_time_entries_question: "%{hours} ساعت روی پیامدهایی Ú©Ù‡ می‌خواهید پاک کنید کار گزارش شده است. می‌خواهید Ú†Ù‡ کنید؟" + text_destroy_time_entries: ساعت‌های گزارش شده پاک شوند + text_assign_time_entries_to_project: ساعت‌های گزارش شده به پروژه واگذار شوند + text_reassign_time_entries: 'ساعت‌های گزارش شده به این پیامد واگذار شوند:' + text_user_wrote: "%{value} نوشت:" + text_enumeration_destroy_question: "%{count} داده به این برشمردنی وابسته شده‌اند." + text_enumeration_category_reassign_to: 'به این برشمردنی وابسته شوند:' + text_email_delivery_not_configured: "Ø¯Ø±ÛŒØ§ÙØª ایمیل پیکربندی نشده است Ùˆ آگاه‌سازی‌ها غیر ÙØ¹Ø§Ù„ هستند.\nکارگزار SMTP خود را در config/configuration.yml پیکربندی کنید Ùˆ برنامه را بازنشانی کنید تا ÙØ¹Ø§Ù„ شوند." + text_repository_usernames_mapping: "کاربر Redmine Ú©Ù‡ به هر نام کاربری پیام‌های انباره نگاشت می‌شود را برگزینید.\nکاربرانی Ú©Ù‡ نام کاربری یا ایمیل همسان دارند، خود به خود نگاشت می‌شوند." + text_diff_truncated: '... این ØªÙØ§ÙˆØª بریده شده چون بیشتر از بیشترین اندازه نمایش دادنی است.' + text_custom_field_possible_values_info: 'یک خط برای هر مقدار' + text_wiki_page_destroy_question: "این برگه %{descendants} زیربرگه دارد.می‌خواهید Ú†Ù‡ کنید؟" + text_wiki_page_nullify_children: "زیربرگه‌ها برگه ریشه شوند" + text_wiki_page_destroy_children: "زیربرگه‌ها Ùˆ زیربرگه‌های آن‌ها پاک شوند" + text_wiki_page_reassign_children: "زیربرگه‌ها به زیر این برگه پدر بروند" + text_own_membership_delete_confirmation: "شما دارید برخی یا همه پروانه‌های خود را برمی‌دارید Ùˆ شاید پس از این دیگر نتوانید این پروژه را ویرایش کنید.\nآیا می‌خواهید این کار را بکنید؟" + text_zoom_in: درشتنمایی + text_zoom_out: ریزنمایی + + default_role_manager: سرپرست + default_role_developer: برنامه‌نویس + default_role_reporter: گزارش‌دهنده + default_tracker_bug: ایراد + default_tracker_feature: ویژگی + default_tracker_support: پشتیبانی + default_issue_status_new: تازه + default_issue_status_in_progress: در گردش + default_issue_status_resolved: درست شده + default_issue_status_feedback: بازخورد + default_issue_status_closed: بسته + default_issue_status_rejected: برگشت خورده + default_doc_category_user: نوشتار کاربر + default_doc_category_tech: نوشتار ÙÙ†ÛŒ + default_priority_low: پایین + default_priority_normal: میانه + default_priority_high: بالا + default_priority_urgent: زود + default_priority_immediate: بیدرنگ + default_activity_design: طراØÛŒ + default_activity_development: ساخت + + enumeration_issue_priorities: برتری‌های پیامد + enumeration_doc_categories: دسته‌های نوشتار + enumeration_activities: کارها (پیگیری زمان) + enumeration_system_activity: کار سامانه + + text_tip_issue_begin_day: پیامد در این روز آغاز می‌شود + field_warn_on_leaving_unsaved: هنگام ترک برگه‌ای Ú©Ù‡ نوشته‌های آن نگهداری نشده، به من هشدار بده + text_tip_issue_begin_end_day: پیامد در این روز آغاز می‌شود Ùˆ پایان می‌پذیرد + text_tip_issue_end_day: پیامد در این روز پایان می‌پذیرد + text_warn_on_leaving_unsaved: این برگه دارای نوشته‌های نگهداری نشده است Ú©Ù‡ اگر آن را ترک کنید، از میان می‌روند. + label_my_queries: جستارهای Ø³ÙØ§Ø±Ø´ÛŒ من + text_journal_changed_no_detail: "%{label} بروز شد" + label_news_comment_added: دیدگاه به یک رویداد Ø§ÙØ²ÙˆØ¯Ù‡ شد + button_expand_all: باز کردن همه + button_collapse_all: بستن همه + label_additional_workflow_transitions_for_assignee: زمانی Ú©Ù‡ به کاربر واگذار شده، گذارهای بیشتر Ù¾Ø°ÛŒØ±ÙØªÙ‡ می‌شود + label_additional_workflow_transitions_for_author: زمانی Ú©Ù‡ کاربر نویسنده است، گذارهای بیشتر Ù¾Ø°ÛŒØ±ÙØªÙ‡ می‌شود + label_bulk_edit_selected_time_entries: ویرایش دسته‌ای زمان‌های گزارش شده گزینش شده + text_time_entries_destroy_confirmation: آیا می‌خواهید زمان‌های گزارش شده گزینش شده پاک شوند؟ + label_role_anonymous: ناشناس + label_role_non_member: غیر عضو + label_issue_note_added: یادداشت Ø§ÙØ²ÙˆØ¯Ù‡ شد + label_issue_status_updated: وضعیت بروز شد + label_issue_priority_updated: برتری بروز شد + label_issues_visibility_own: Issues created by or assigned to the user + field_issues_visibility: Issues visibility + label_issues_visibility_all: All issues + permission_set_own_issues_private: Set own issues public or private + field_is_private: Private + permission_set_issues_private: Set issues public or private + label_issues_visibility_public: All non private issues + text_issues_destroy_descendants_confirmation: This will also delete %{count} subtask(s). + field_commit_logs_encoding: کدگذاری پیام‌های انباره + field_scm_path_encoding: Path encoding + text_scm_path_encoding_note: "Default: UTF-8" + field_path_to_repository: Path to repository + field_root_directory: Root directory + field_cvs_module: Module + field_cvsroot: CVSROOT + text_mercurial_repository_note: Local repository (e.g. /hgrepo, c:\hgrepo) + text_scm_command: Command + text_scm_command_version: Version + label_git_report_last_commit: Report last commit for files and directories + notice_issue_successful_create: Issue %{id} created. + label_between: between + setting_issue_group_assignment: Allow issue assignment to groups + label_diff: diff + text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo) + description_query_sort_criteria_direction: Sort direction + description_project_scope: Search scope + description_filter: Filter + description_user_mail_notification: Mail notification settings + description_date_from: Enter start date + description_message_content: Message content + description_available_columns: Available Columns + description_date_range_interval: Choose range by selecting start and end date + description_issue_category_reassign: Choose issue category + description_search: Searchfield + description_notes: Notes + description_date_range_list: Choose range from list + description_choose_project: Projects + description_date_to: Enter end date + description_query_sort_criteria_attribute: Sort attribute + description_wiki_subpages_reassign: Choose new parent page + description_selected_columns: Selected Columns + label_parent_revision: Parent + label_child_revision: Child + error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size. + setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues + button_edit_section: Edit this section + setting_repositories_encodings: Attachments and repositories encodings + description_all_columns: All Columns + button_export: Export + label_export_options: "%{export_format} export options" + error_attachment_too_big: This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size}) + notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}." + label_x_issues: + zero: 0 پیامد + one: 1 پیامد + other: "%{count} پیامد" + label_repository_new: New repository + field_repository_is_default: Main repository + label_copy_attachments: Copy attachments + label_item_position: "%{position}/%{count}" + label_completed_versions: Completed versions + text_project_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.+<%= label_tag "user_mail_notification", l(:description_user_mail_notification), :class => "hidden-for-sighted" %> +<%= select_tag( + 'user[mail_notification]', + options_for_select( + user_mail_notification_options(@user), @user.mail_notification), + :onchange => 'if (this.value == "selected") {$("#notified-projects").show();} else {$("#notified-projects").hide();}' + ) %> +
+<%= content_tag 'div', :id => 'notified-projects', :style => (@user.mail_notification == 'selected' ? '' : 'display:none;') do %> + <%= render_project_nested_lists(@user.projects) do |project| + content_tag('label', + check_box_tag( + 'user[notified_project_ids][]', + project.id, + @user.notified_projects_ids.include?(project.id), + :id => nil + ) + ' ' + h(project.name) + ) + end %> + <%= hidden_field_tag 'user[notified_project_ids][]', '' %> +<%= l(:text_user_mail_option) %>
+<% end %> + +<%= fields_for :pref, @user.pref do |pref_fields| %> ++ <%= pref_fields.check_box :no_self_notified %> + +
+<% end %> diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/07/07ff3aff4af2ca5266644ab7e838820cda8c8752.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/07/07ff3aff4af2ca5266644ab7e838820cda8c8752.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,27 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 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 File.expand_path('../../../test_helper', __FILE__) + +class RoutingAutoCompletesTest < ActionController::IntegrationTest + def test_auto_completes + assert_routing( + { :method => 'get', :path => "/issues/auto_complete" }, + { :controller => 'auto_completes', :action => 'issues' } + ) + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/08/086f4bbd984edf33971e918b1e5330fe445e9dd7.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/08/086f4bbd984edf33971e918b1e5330fe445e9dd7.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,253 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 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 File.expand_path('../../../../../test_helper', __FILE__) + +class Redmine::MenuManager::MenuHelperTest < ActionView::TestCase + + include Redmine::MenuManager::MenuHelper + include ERB::Util + fixtures :users, :members, :projects, :enabled_modules, :roles, :member_roles + + def setup + setup_with_controller + # Stub the current menu item in the controller + def current_menu_item + :index + end + end + + context "MenuManager#current_menu_item" do + should "be tested" + end + + context "MenuManager#render_main_menu" do + should "be tested" + end + + context "MenuManager#render_menu" do + should "be tested" + end + + context "MenuManager#menu_item_and_children" do + should "be tested" + end + + context "MenuManager#extract_node_details" do + should "be tested" + end + + def test_render_single_menu_node + node = Redmine::MenuManager::MenuItem.new(:testing, '/test', { }) + @output_buffer = render_single_menu_node(node, 'This is a test', node.url, false) + + assert_select("a.testing", "This is a test") + end + + def test_render_menu_node + single_node = Redmine::MenuManager::MenuItem.new(:single_node, '/test', { }) + @output_buffer = render_menu_node(single_node, nil) + + assert_select("li") do + assert_select("a.single-node", "Single node") + end + end + + def test_render_menu_node_with_nested_items + parent_node = Redmine::MenuManager::MenuItem.new(:parent_node, '/test', { }) + parent_node << Redmine::MenuManager::MenuItem.new(:child_one_node, '/test', { }) + parent_node << Redmine::MenuManager::MenuItem.new(:child_two_node, '/test', { }) + parent_node << + Redmine::MenuManager::MenuItem.new(:child_three_node, '/test', { }) << + Redmine::MenuManager::MenuItem.new(:child_three_inner_node, '/test', { }) + + @output_buffer = render_menu_node(parent_node, nil) + + assert_select("li") do + assert_select("a.parent-node", "Parent node") + assert_select("ul") do + assert_select("li a.child-one-node", "Child one node") + assert_select("li a.child-two-node", "Child two node") + assert_select("li") do + assert_select("a.child-three-node", "Child three node") + assert_select("ul") do + assert_select("li a.child-three-inner-node", "Child three inner node") + end + end + end + end + + end + + def test_render_menu_node_with_children + User.current = User.find(2) + + parent_node = Redmine::MenuManager::MenuItem.new(:parent_node, + '/test', + { + :children => Proc.new {|p| + children = [] + 3.times do |time| + children << Redmine::MenuManager::MenuItem.new("test_child_#{time}", + {:controller => 'issues', :action => 'index'}, + {}) + end + children + } + }) + @output_buffer = render_menu_node(parent_node, Project.find(1)) + + assert_select("li") do + assert_select("a.parent-node", "Parent node") + assert_select("ul") do + assert_select("li a.test-child-0", "Test child 0") + assert_select("li a.test-child-1", "Test child 1") + assert_select("li a.test-child-2", "Test child 2") + end + end + end + + def test_render_menu_node_with_nested_items_and_children + User.current = User.find(2) + + parent_node = Redmine::MenuManager::MenuItem.new(:parent_node, + '/test', + { + :children => Proc.new {|p| + children = [] + 3.times do |time| + children << Redmine::MenuManager::MenuItem.new("test_child_#{time}", {:controller => 'issues', :action => 'index'}, {}) + end + children + } + }) + + parent_node << Redmine::MenuManager::MenuItem.new(:child_node, + '/test', + { + :children => Proc.new {|p| + children = [] + 6.times do |time| + children << Redmine::MenuManager::MenuItem.new("test_dynamic_child_#{time}", {:controller => 'issues', :action => 'index'}, {}) + end + children + } + }) + + @output_buffer = render_menu_node(parent_node, Project.find(1)) + + assert_select("li") do + assert_select("a.parent-node", "Parent node") + assert_select("ul") do + assert_select("li a.child-node", "Child node") + assert_select("ul") do + assert_select("li a.test-dynamic-child-0", "Test dynamic child 0") + assert_select("li a.test-dynamic-child-1", "Test dynamic child 1") + assert_select("li a.test-dynamic-child-2", "Test dynamic child 2") + assert_select("li a.test-dynamic-child-3", "Test dynamic child 3") + assert_select("li a.test-dynamic-child-4", "Test dynamic child 4") + assert_select("li a.test-dynamic-child-5", "Test dynamic child 5") + end + assert_select("li a.test-child-0", "Test child 0") + assert_select("li a.test-child-1", "Test child 1") + assert_select("li a.test-child-2", "Test child 2") + end + end + end + + def test_render_menu_node_with_children_without_an_array + parent_node = Redmine::MenuManager::MenuItem.new(:parent_node, + '/test', + { + :children => Proc.new {|p| Redmine::MenuManager::MenuItem.new("test_child", "/testing", {})} + }) + + assert_raises Redmine::MenuManager::MenuError, ":children must be an array of MenuItems" do + @output_buffer = render_menu_node(parent_node, Project.find(1)) + end + end + + def test_render_menu_node_with_incorrect_children + parent_node = Redmine::MenuManager::MenuItem.new(:parent_node, + '/test', + { + :children => Proc.new {|p| ["a string"] } + }) + + assert_raises Redmine::MenuManager::MenuError, ":children must be an array of MenuItems" do + @output_buffer = render_menu_node(parent_node, Project.find(1)) + end + + end + + def test_menu_items_for_should_yield_all_items_if_passed_a_block + menu_name = :test_menu_items_for_should_yield_all_items_if_passed_a_block + Redmine::MenuManager.map menu_name do |menu| + menu.push(:a_menu, '/', { }) + menu.push(:a_menu_2, '/', { }) + menu.push(:a_menu_3, '/', { }) + end + + items_yielded = [] + menu_items_for(menu_name) do |item| + items_yielded << item + end + + assert_equal 3, items_yielded.size + end + + def test_menu_items_for_should_return_all_items + menu_name = :test_menu_items_for_should_return_all_items + Redmine::MenuManager.map menu_name do |menu| + menu.push(:a_menu, '/', { }) + menu.push(:a_menu_2, '/', { }) + menu.push(:a_menu_3, '/', { }) + end + + items = menu_items_for(menu_name) + assert_equal 3, items.size + end + + def test_menu_items_for_should_skip_unallowed_items_on_a_project + menu_name = :test_menu_items_for_should_skip_unallowed_items_on_a_project + Redmine::MenuManager.map menu_name do |menu| + menu.push(:a_menu, {:controller => 'issues', :action => 'index' }, { }) + menu.push(:a_menu_2, {:controller => 'issues', :action => 'index' }, { }) + menu.push(:unallowed, {:controller => 'issues', :action => 'unallowed' }, { }) + end + + User.current = User.find(2) + + items = menu_items_for(menu_name, Project.find(1)) + assert_equal 2, items.size + end + + def test_menu_items_for_should_skip_items_that_fail_the_conditions + menu_name = :test_menu_items_for_should_skip_items_that_fail_the_conditions + Redmine::MenuManager.map menu_name do |menu| + menu.push(:a_menu, {:controller => 'issues', :action => 'index' }, { }) + menu.push(:unallowed, + {:controller => 'issues', :action => 'index' }, + { :if => Proc.new { false }}) + end + + User.current = User.find(2) + + items = menu_items_for(menu_name, Project.find(1)) + assert_equal 1, items.size + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/08/088eae42f08379f3106075b388f036d1754d148c.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/08/088eae42f08379f3106075b388f036d1754d148c.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,21 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2013 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. + +module EnumerationsHelper +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/08/08918fc8f853142db140298c79057a45064d5050.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/08/08918fc8f853142db140298c79057a45064d5050.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,72 @@ +desc "Run the Continous Integration tests for Redmine" +task :ci do + # RAILS_ENV and ENV[] can diverge so force them both to test + ENV['RAILS_ENV'] = 'test' + RAILS_ENV = 'test' + Rake::Task["ci:setup"].invoke + Rake::Task["ci:build"].invoke + Rake::Task["ci:teardown"].invoke +end + +namespace :ci do + desc "Setup Redmine for a new build" + task :setup do + Rake::Task["tmp:clear"].invoke + Rake::Task["log:clear"].invoke + Rake::Task["db:create:all"].invoke + Rake::Task["db:migrate"].invoke + Rake::Task["db:schema:dump"].invoke + Rake::Task["test:scm:setup:all"].invoke + Rake::Task["test:scm:update"].invoke + end + + desc "Build Redmine" + task :build do + Rake::Task["test"].invoke + # Rake::Task["test:ui"].invoke if RUBY_VERSION >= '1.9.3' + end + + desc "Finish the build" + task :teardown do + end +end + +desc "Creates database.yml for the CI server" +file 'config/database.yml' do + require 'yaml' + database = ENV['DATABASE_ADAPTER'] + ruby = ENV['RUBY_VER'].gsub('.', '').gsub('-', '') + branch = ENV['BRANCH'].gsub('.', '').gsub('-', '') + dev_db_name = "ci_#{branch}_#{ruby}_dev" + test_db_name = "ci_#{branch}_#{ruby}_test" + + case database + when 'mysql' + dev_conf = {'adapter' => (RUBY_VERSION >= '1.9' ? 'mysql2' : 'mysql'), + 'database' => dev_db_name, 'host' => 'localhost', + 'username' => 'jenkins', 'password' => 'jenkins', + 'encoding' => 'utf8'} + test_conf = dev_conf.merge('database' => test_db_name) + when 'postgresql' + dev_conf = {'adapter' => 'postgresql', 'database' => dev_db_name, + 'host' => 'localhost', + 'username' => 'jenkins', 'password' => 'jenkins'} + test_conf = dev_conf.merge('database' => test_db_name) + when /sqlite3/ + dev_conf = {'adapter' => (Object.const_defined?(:JRUBY_VERSION) ? + 'jdbcsqlite3' : 'sqlite3'), + 'database' => "db/#{dev_db_name}.sqlite3"} + test_conf = dev_conf.merge('database' => "db/#{test_db_name}.sqlite3") + when 'sqlserver' + dev_conf = {'adapter' => 'sqlserver', 'database' => dev_db_name, + 'host' => 'mssqlserver', 'port' => 1433, + 'username' => 'jenkins', 'password' => 'jenkins'} + test_conf = dev_conf.merge('database' => test_db_name) + else + abort "Unknown database" + end + + File.open('config/database.yml', 'w') do |f| + f.write YAML.dump({'development' => dev_conf, 'test' => test_conf}) + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/08/08c1f1410c30c4d18ef6cbcbd8954d1721fb044b.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/08/08c1f1410c30c4d18ef6cbcbd8954d1721fb044b.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,195 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 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 File.expand_path('../../test_helper', __FILE__) + +class WatchersControllerTest < ActionController::TestCase + fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, + :issues, :trackers, :projects_trackers, :issue_statuses, :enumerations, :watchers + + def setup + User.current = nil + end + + def test_watch_a_single_object + @request.session[:user_id] = 3 + assert_difference('Watcher.count') do + xhr :post, :watch, :object_type => 'issue', :object_id => '1' + assert_response :success + assert_include '$(".issue-1-watcher")', response.body + end + assert Issue.find(1).watched_by?(User.find(3)) + end + + def test_watch_a_collection_with_a_single_object + @request.session[:user_id] = 3 + assert_difference('Watcher.count') do + xhr :post, :watch, :object_type => 'issue', :object_id => ['1'] + assert_response :success + assert_include '$(".issue-1-watcher")', response.body + end + assert Issue.find(1).watched_by?(User.find(3)) + end + + def test_watch_a_collection_with_multiple_objects + @request.session[:user_id] = 3 + assert_difference('Watcher.count', 2) do + xhr :post, :watch, :object_type => 'issue', :object_id => ['1', '3'] + assert_response :success + assert_include '$(".issue-bulk-watcher")', response.body + end + assert Issue.find(1).watched_by?(User.find(3)) + assert Issue.find(3).watched_by?(User.find(3)) + end + + def test_watch_should_be_denied_without_permission + Role.find(2).remove_permission! :view_issues + @request.session[:user_id] = 3 + assert_no_difference('Watcher.count') do + xhr :post, :watch, :object_type => 'issue', :object_id => '1' + assert_response 403 + end + end + + def test_watch_invalid_class_should_respond_with_404 + @request.session[:user_id] = 3 + assert_no_difference('Watcher.count') do + xhr :post, :watch, :object_type => 'foo', :object_id => '1' + assert_response 404 + end + end + + def test_watch_invalid_object_should_respond_with_404 + @request.session[:user_id] = 3 + assert_no_difference('Watcher.count') do + xhr :post, :watch, :object_type => 'issue', :object_id => '999' + assert_response 404 + end + end + + def test_unwatch + @request.session[:user_id] = 3 + assert_difference('Watcher.count', -1) do + xhr :delete, :unwatch, :object_type => 'issue', :object_id => '2' + assert_response :success + assert_include '$(".issue-2-watcher")', response.body + end + assert !Issue.find(1).watched_by?(User.find(3)) + end + + def test_unwatch_a_collection_with_multiple_objects + @request.session[:user_id] = 3 + Watcher.create!(:user_id => 3, :watchable => Issue.find(1)) + Watcher.create!(:user_id => 3, :watchable => Issue.find(3)) + + assert_difference('Watcher.count', -2) do + xhr :delete, :unwatch, :object_type => 'issue', :object_id => ['1', '3'] + assert_response :success + assert_include '$(".issue-bulk-watcher")', response.body + end + assert !Issue.find(1).watched_by?(User.find(3)) + assert !Issue.find(3).watched_by?(User.find(3)) + end + + def test_new + @request.session[:user_id] = 2 + xhr :get, :new, :object_type => 'issue', :object_id => '2' + assert_response :success + assert_match /ajax-modal/, response.body + end + + def test_new_for_new_record_with_project_id + @request.session[:user_id] = 2 + xhr :get, :new, :project_id => 1 + assert_response :success + assert_equal Project.find(1), assigns(:project) + assert_match /ajax-modal/, response.body + end + + def test_new_for_new_record_with_project_identifier + @request.session[:user_id] = 2 + xhr :get, :new, :project_id => 'ecookbook' + assert_response :success + assert_equal Project.find(1), assigns(:project) + assert_match /ajax-modal/, response.body + end + + def test_create + @request.session[:user_id] = 2 + assert_difference('Watcher.count') do + xhr :post, :create, :object_type => 'issue', :object_id => '2', :watcher => {:user_id => '4'} + assert_response :success + assert_match /watchers/, response.body + assert_match /ajax-modal/, response.body + end + assert Issue.find(2).watched_by?(User.find(4)) + end + + def test_create_multiple + @request.session[:user_id] = 2 + assert_difference('Watcher.count', 2) do + xhr :post, :create, :object_type => 'issue', :object_id => '2', :watcher => {:user_ids => ['4', '7']} + assert_response :success + assert_match /watchers/, response.body + assert_match /ajax-modal/, response.body + end + assert Issue.find(2).watched_by?(User.find(4)) + assert Issue.find(2).watched_by?(User.find(7)) + end + + def test_autocomplete_on_watchable_creation + @request.session[:user_id] = 2 + xhr :get, :autocomplete_for_user, :q => 'mi', :project_id => 'ecookbook' + assert_response :success + assert_select 'input', :count => 4 + assert_select 'input[name=?][value=1]', 'watcher[user_ids][]' + assert_select 'input[name=?][value=2]', 'watcher[user_ids][]' + assert_select 'input[name=?][value=8]', 'watcher[user_ids][]' + assert_select 'input[name=?][value=9]', 'watcher[user_ids][]' + end + + def test_autocomplete_on_watchable_update + @request.session[:user_id] = 2 + xhr :get, :autocomplete_for_user, :q => 'mi', :object_id => '2' , :object_type => 'issue', :project_id => 'ecookbook' + assert_response :success + assert_select 'input', :count => 3 + assert_select 'input[name=?][value=2]', 'watcher[user_ids][]' + assert_select 'input[name=?][value=8]', 'watcher[user_ids][]' + assert_select 'input[name=?][value=9]', 'watcher[user_ids][]' + + end + + def test_append + @request.session[:user_id] = 2 + assert_no_difference 'Watcher.count' do + xhr :post, :append, :watcher => {:user_ids => ['4', '7']}, :project_id => 'ecookbook' + assert_response :success + assert_include 'watchers_inputs', response.body + assert_include 'issue[watcher_user_ids][]', response.body + end + end + + def test_remove_watcher + @request.session[:user_id] = 2 + assert_difference('Watcher.count', -1) do + xhr :delete, :destroy, :object_type => 'issue', :object_id => '2', :user_id => '3' + assert_response :success + assert_match /watchers/, response.body + end + assert !Issue.find(2).watched_by?(User.find(3)) + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/08/08d8c21c9156481395a983e5e0a4c9f73ad91ca9.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/08/08d8c21c9156481395a983e5e0a4c9f73ad91ca9.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,71 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 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 File.expand_path('../../../../../../test_helper', __FILE__) + +begin + require 'mocha' + + class SubversionAdapterTest < ActiveSupport::TestCase + + if repository_configured?('subversion') + def setup + @adapter = Redmine::Scm::Adapters::SubversionAdapter.new(self.class.subversion_repository_url) + end + + def test_client_version + v = Redmine::Scm::Adapters::SubversionAdapter.client_version + assert v.is_a?(Array) + end + + def test_scm_version + to_test = { "svn, version 1.6.13 (r1002816)\n" => [1,6,13], + "svn, versione 1.6.13 (r1002816)\n" => [1,6,13], + "1.6.1\n1.7\n1.8" => [1,6,1], + "1.6.2\r\n1.8.1\r\n1.9.1" => [1,6,2]} + to_test.each do |s, v| + test_scm_version_for(s, v) + end + end + + def test_info_not_nil + assert_not_nil @adapter.info + end + + def test_info_nil + adpt = Redmine::Scm::Adapters::SubversionAdapter.new( + "file:///invalid/invalid/" + ) + assert_nil adpt.info + end + + private + + def test_scm_version_for(scm_version, version) + @adapter.class.expects(:scm_version_from_command_line).returns(scm_version) + assert_equal version, @adapter.class.svn_binary_version + end + else + puts "Subversion test repository NOT FOUND. Skipping unit tests !!!" + def test_fake; assert true end + end + end +rescue LoadError + class SubversionMochaFake < ActiveSupport::TestCase + def test_fake; assert(false, "Requires mocha to run those tests") end + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/09/094f0fabb02fa5ce798642abc8745a5aba89d2d9.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/09/094f0fabb02fa5ce798642abc8745a5aba89d2d9.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,175 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 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 File.expand_path('../../test_helper', __FILE__) + +class AccountControllerOpenidTest < ActionController::TestCase + tests AccountController + fixtures :users, :roles + + def setup + User.current = nil + Setting.openid = '1' + end + + def teardown + Setting.openid = '0' + end + + if Object.const_defined?(:OpenID) + + def test_login_with_openid_for_existing_user + Setting.self_registration = '3' + existing_user = User.new(:firstname => 'Cool', + :lastname => 'User', + :mail => 'user@somedomain.com', + :identity_url => 'http://openid.example.com/good_user') + existing_user.login = 'cool_user' + assert existing_user.save! + + post :login, :openid_url => existing_user.identity_url + assert_redirected_to '/my/page' + end + + def test_login_with_invalid_openid_provider + Setting.self_registration = '0' + post :login, :openid_url => 'http;//openid.example.com/good_user' + assert_redirected_to home_url + end + + def test_login_with_openid_for_existing_non_active_user + Setting.self_registration = '2' + existing_user = User.new(:firstname => 'Cool', + :lastname => 'User', + :mail => 'user@somedomain.com', + :identity_url => 'http://openid.example.com/good_user', + :status => User::STATUS_REGISTERED) + existing_user.login = 'cool_user' + assert existing_user.save! + + post :login, :openid_url => existing_user.identity_url + assert_redirected_to '/login' + end + + def test_login_with_openid_with_new_user_created + Setting.self_registration = '3' + post :login, :openid_url => 'http://openid.example.com/good_user' + assert_redirected_to '/my/account' + user = User.find_by_login('cool_user') + assert user + assert_equal 'Cool', user.firstname + assert_equal 'User', user.lastname + end + + def test_login_with_openid_with_new_user_and_self_registration_off + Setting.self_registration = '0' + post :login, :openid_url => 'http://openid.example.com/good_user' + assert_redirected_to home_url + user = User.find_by_login('cool_user') + assert_nil user + end + + def test_login_with_openid_with_new_user_created_with_email_activation_should_have_a_token + Setting.self_registration = '1' + post :login, :openid_url => 'http://openid.example.com/good_user' + assert_redirected_to '/login' + user = User.find_by_login('cool_user') + assert user + + token = Token.find_by_user_id_and_action(user.id, 'register') + assert token + end + + def test_login_with_openid_with_new_user_created_with_manual_activation + Setting.self_registration = '2' + post :login, :openid_url => 'http://openid.example.com/good_user' + assert_redirected_to '/login' + user = User.find_by_login('cool_user') + assert user + assert_equal User::STATUS_REGISTERED, user.status + end + + def test_login_with_openid_with_new_user_with_conflict_should_register + Setting.self_registration = '3' + existing_user = User.new(:firstname => 'Cool', :lastname => 'User', :mail => 'user@somedomain.com') + existing_user.login = 'cool_user' + assert existing_user.save! + + post :login, :openid_url => 'http://openid.example.com/good_user' + assert_response :success + assert_template 'register' + assert assigns(:user) + assert_equal 'http://openid.example.com/good_user', assigns(:user)[:identity_url] + end + + def test_login_with_openid_with_new_user_with_missing_information_should_register + Setting.self_registration = '3' + + post :login, :openid_url => 'http://openid.example.com/good_blank_user' + assert_response :success + assert_template 'register' + assert assigns(:user) + assert_equal 'http://openid.example.com/good_blank_user', assigns(:user)[:identity_url] + + assert_select 'input[name=?]', 'user[login]' + assert_select 'input[name=?]', 'user[password]' + assert_select 'input[name=?]', 'user[password_confirmation]' + assert_select 'input[name=?][value=?]', 'user[identity_url]', 'http://openid.example.com/good_blank_user' + end + + def test_post_login_should_not_verify_token_when_using_open_id + ActionController::Base.allow_forgery_protection = true + AccountController.any_instance.stubs(:using_open_id?).returns(true) + AccountController.any_instance.stubs(:authenticate_with_open_id).returns(true) + post :login + assert_response 200 + ensure + ActionController::Base.allow_forgery_protection = false + end + + def test_register_after_login_failure_should_not_require_user_to_enter_a_password + Setting.self_registration = '3' + + assert_difference 'User.count' do + post :register, :user => { + :login => 'good_blank_user', + :password => '', + :password_confirmation => '', + :firstname => 'Cool', + :lastname => 'User', + :mail => 'user@somedomain.com', + :identity_url => 'http://openid.example.com/good_blank_user' + } + assert_response 302 + end + + user = User.first(:order => 'id DESC') + assert_equal 'http://openid.example.com/good_blank_user', user.identity_url + assert user.hashed_password.blank?, "Hashed password was #{user.hashed_password}" + end + + def test_setting_openid_should_return_true_when_set_to_true + assert_equal true, Setting.openid? + end + + else + puts "Skipping openid tests." + + def test_dummy + end + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/09/09c2ce49203d7971d2ffb1539d86601fbcb17a3b.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/09/09c2ce49203d7971d2ffb1539d86601fbcb17a3b.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,93 @@ +<%= l(:label_no_data) %>
+<% else %> +<%= check_box_tag 'ids[]', issue.id, false, :id => nil %> | +<%= link_to_issue(issue, :project => (@project != issue.project)) %> | +
<%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %>
+<% end %> + ++ <%= link_to_function l(:label_completed_versions), + '$("#toggle-completed-versions").toggleClass("collapsed"); $("#completed-versions").toggle()', + :id => 'toggle-completed-versions', :class => 'collapsible collapsed' %> +
+ +<% end %> +<% end %> + +<% html_title(l(:label_roadmap)) %> + +<%= context_menu issues_context_menu_path %> diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/09/09d005add00d07aeb449a6c04ad00db8e1f72fbd.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/09/09d005add00d07aeb449a6c04ad00db8e1f72fbd.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,73 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 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 File.expand_path('../../../test_helper', __FILE__) + +class RoutingMyTest < ActionController::IntegrationTest + def test_my + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/my/account" }, + { :controller => 'my', :action => 'account' } + ) + end + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/my/account/destroy" }, + { :controller => 'my', :action => 'destroy' } + ) + end + assert_routing( + { :method => 'get', :path => "/my/page" }, + { :controller => 'my', :action => 'page' } + ) + assert_routing( + { :method => 'get', :path => "/my" }, + { :controller => 'my', :action => 'index' } + ) + assert_routing( + { :method => 'post', :path => "/my/reset_rss_key" }, + { :controller => 'my', :action => 'reset_rss_key' } + ) + assert_routing( + { :method => 'post', :path => "/my/reset_api_key" }, + { :controller => 'my', :action => 'reset_api_key' } + ) + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/my/password" }, + { :controller => 'my', :action => 'password' } + ) + end + assert_routing( + { :method => 'get', :path => "/my/page_layout" }, + { :controller => 'my', :action => 'page_layout' } + ) + assert_routing( + { :method => 'post', :path => "/my/add_block" }, + { :controller => 'my', :action => 'add_block' } + ) + assert_routing( + { :method => 'post', :path => "/my/remove_block" }, + { :controller => 'my', :action => 'remove_block' } + ) + assert_routing( + { :method => 'post', :path => "/my/order_blocks" }, + { :controller => 'my', :action => 'order_blocks' } + ) + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/09/09e163f7a5ba48f41a7d0433361b8d9b46760493.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/09/09e163f7a5ba48f41a7d0433361b8d9b46760493.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,55 @@ +<%= error_messages_for 'query' %> + ++<%= text_field 'query', 'name', :size => 80 %>
+ +<% if User.current.admin? || User.current.allowed_to?(:manage_public_queries, @project) %> ++<%= check_box 'query', 'is_public', + :onchange => (User.current.admin? ? nil : 'if (this.checked) {$("#query_is_for_all").removeAttr("checked"); $("#query_is_for_all").attr("disabled", true);} else {$("#query_is_for_all").removeAttr("disabled");}') %>
+<% end %> + ++<%= check_box_tag 'query_is_for_all', 1, @query.project.nil?, + :disabled => (!@query.new_record? && (@query.project.nil? || (@query.is_public? && !User.current.admin?))) %>
+ ++<%= check_box_tag 'default_columns', 1, @query.has_default_columns?, :id => 'query_default_columns', + :onclick => 'if (this.checked) {$("#columns").hide();} else {$("#columns").show();}' %>
+ ++<%= select 'query', 'group_by', @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, :include_blank => true %>
+ ++<%= available_block_columns_tags(@query) %>
+'
+ else
+ m_sl = shebang_re.match(line)
+ if m_sl
+ shebang_line = true
+ line = ''
+ end
+ m_pre_end = pre_end_re.match(line)
+ if m_pre_end
+ line = '
'
+ if shebang_line
+ line = '' + line
+ end
+ end
+ end
+ line
+ end
+
+ # Highlighting
+ text = text.gsub(/'''''([^\s])/, '_*\1')
+ text = text.gsub(/([^\s])'''''/, '\1*_')
+ text = text.gsub(/'''/, '*')
+ 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_attachments = 0
+
+ #Wiki system initializing...
+ @target_project.wiki.destroy if @target_project.wiki
+ @target_project.reload
+ wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
+ wiki_edit_count = 0
+
+ # Components
+ print "Migrating components"
+ issues_category_map = {}
+ TracComponent.all.each do |component|
+ 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
+ end
+ puts
+
+ # Milestones
+ print "Migrating milestones"
+ version_map = {}
+ TracMilestone.all.each do |milestone|
+ print '.'
+ STDOUT.flush
+ # First we try to find the wiki page...
+ p = wiki.find_or_new_page(milestone.name.to_s)
+ p.content = WikiContent.new(:page => p) if p.new_record?
+ p.content.text = milestone.description.to_s
+ p.content.author = find_or_create_user('trac')
+ p.content.comments = 'Milestone'
+ p.save
+
+ v = Version.new :project => @target_project,
+ :name => encode(milestone.name[0, limit_for(Version, 'name')]),
+ :description => nil,
+ :wiki_page_title => milestone.name.to_s,
+ :effective_date => milestone.completed
+
+ next unless v.save
+ version_map[milestone.name] = v
+ migrated_milestones += 1
+ end
+ puts
+
+ # Custom fields
+ # TODO: read trac.ini instead
+ print "Migrating custom fields"
+ custom_field_map = {}
+ TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
+ print '.'
+ STDOUT.flush
+ # Redmine custom field name
+ field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
+ # Find if the custom already exists in Redmine
+ f = IssueCustomField.find_by_name(field_name)
+ # 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.all
+ f.projects << @target_project
+ custom_field_map[field.name] = f
+ end
+ puts
+
+ # Trac 'resolution' field as a Redmine custom field
+ r = IssueCustomField.where(:name => "Resolution").first
+ r = IssueCustomField.new(:name => 'Resolution',
+ :field_format => 'list',
+ :is_filter => true) if r.nil?
+ r.trackers = Tracker.all
+ r.projects << @target_project
+ 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_each(:batch_size => 200) do |ticket|
+ 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.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.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 &&
+ STATUS_MAPPING[status_change.oldvalue] &&
+ STATUS_MAPPING[status_change.newvalue] &&
+ (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
+ n.details << JournalDetail.new(:property => 'attr',
+ :prop_key => 'status_id',
+ :old_value => STATUS_MAPPING[status_change.oldvalue].id,
+ :value => STATUS_MAPPING[status_change.newvalue].id)
+ end
+ if resolution_change
+ n.details << JournalDetail.new(:property => 'cf',
+ :prop_key => custom_field_map['resolution'].id,
+ :old_value => resolution_change.oldvalue,
+ :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?
+ attachment.open {
+ 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
+ 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
+ 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
+ print "Migrating wiki"
+ if wiki.save
+ TracWikiPage.order('name, version').all.each do |page|
+ # Do not migrate Trac manual wiki pages
+ next if TRAC_WIKI_PAGES.include?(page.name)
+ wiki_edit_count += 1
+ print '.'
+ STDOUT.flush
+ p = wiki.find_or_new_page(page.name)
+ p.content = WikiContent.new(:page => p) if p.new_record?
+ p.content.text = page.text
+ 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
+
+ # Attachments
+ page.attachments.each do |attachment|
+ next unless attachment.exist?
+ next if p.attachments.find_by_filename(attachment.filename.gsub(/^.*(\\|\/)/, '').gsub(/[^\w\.\-]/,'_')) #add only once per page
+ attachment.open {
+ a = Attachment.new :created_on => attachment.time
+ a.file = attachment
+ a.author = find_or_create_user(attachment.author)
+ a.description = attachment.description
+ a.container = p
+ 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)
+ Time.fake(page.content.updated_on) { page.content.save }
+ end
+ end
+ puts
+
+ puts
+ puts "Components: #{migrated_components}/#{TracComponent.count}"
+ puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}"
+ puts "Tickets: #{migrated_tickets}/#{TracTicket.count}"
+ puts "Ticket files: #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s
+ puts "Custom values: #{migrated_custom_values}/#{TracTicketCustom.count}"
+ 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)
+ @charset = charset
+ end
+
+ def self.set_trac_directory(path)
+ @@trac_directory = path
+ raise "This directory doesn't exist!" unless File.directory?(path)
+ raise "#{trac_attachments_directory} doesn't exist!" unless File.directory?(trac_attachments_directory)
+ @@trac_directory
+ rescue Exception => e
+ puts e
+ return false
+ end
+
+ def self.trac_directory
+ @@trac_directory
+ end
+
+ def self.set_trac_adapter(adapter)
+ return false if adapter.blank?
+ raise "Unknown adapter: #{adapter}!" unless %w(sqlite3 mysql postgresql).include?(adapter)
+ # If adapter is sqlite or sqlite3, make sure that trac.db exists
+ raise "#{trac_db_path} doesn't exist!" if %w(sqlite3).include?(adapter) && !File.exist?(trac_db_path)
+ @@trac_adapter = adapter
+ rescue Exception => e
+ puts e
+ return false
+ end
+
+ def self.set_trac_db_host(host)
+ return nil if host.blank?
+ @@trac_db_host = host
+ end
+
+ def self.set_trac_db_port(port)
+ 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
+ end
+
+ 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)
+ if !project
+ # create the target project
+ project = Project.new :name => identifier.humanize,
+ :description => ''
+ project.identifier = identifier
+ puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
+ # enable issues and wiki for the created project
+ project.enabled_module_names = ['issue_tracking', 'wiki']
+ else
+ puts
+ puts "This project already exists in your Redmine database."
+ print "Are you sure you want to append data to this project ? [Y/n] "
+ STDOUT.flush
+ 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
+ @target_project.reload
+ end
+
+ def self.connection_params
+ if trac_adapter == 'sqlite3'
+ {:adapter => 'sqlite3',
+ :database => trac_db_path}
+ else
+ {:adapter => trac_adapter,
+ :database => trac_db_name,
+ :host => trac_db_host,
+ :port => trac_db_port,
+ :username => trac_db_username,
+ :password => trac_db_password,
+ :schema_search_path => trac_db_schema
+ }
+ end
+ end
+
+ def self.establish_connection
+ constants.each do |const|
+ klass = const_get(const)
+ next unless klass.respond_to? 'establish_connection'
+ klass.establish_connection connection_params
+ end
+ end
+
+ def self.encode(text)
+ if RUBY_VERSION < '1.9'
+ @ic ||= Iconv.new('UTF-8', @charset)
+ @ic.iconv text
+ else
+ text.to_s.force_encoding(@charset).encode('UTF-8')
+ end
+ end
+ end
+
+ puts
+ if Redmine::DefaultData::Loader.no_data?
+ puts "Redmine configuration need to be loaded before importing data."
+ puts "Please, run this first:"
+ puts
+ 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] "
+ STDOUT.flush
+ break unless STDIN.gets.match(/^y$/i)
+ puts
+
+ def prompt(text, options = {}, &block)
+ default = options[:default] || ''
+ while true
+ print "#{text} [#{default}]: "
+ STDOUT.flush
+ value = STDIN.gets.chomp!
+ value = default if value.blank?
+ 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 (sqlite3, mysql2, postgresql)', :default => 'sqlite3') {|adapter| TracMigrate.set_trac_adapter adapter}
+ unless %w(sqlite3).include?(TracMigrate.trac_adapter)
+ prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
+ prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
+ prompt('Trac database name') {|name| TracMigrate.set_trac_db_name name}
+ prompt('Trac database schema', :default => 'public') {|schema| TracMigrate.set_trac_db_schema schema}
+ prompt('Trac database username') {|username| TracMigrate.set_trac_db_username username}
+ prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
+ end
+ prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
+ prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
+ puts
+
+ old_notified_events = Setting.notified_events
+ old_password_min_length = Setting.password_min_length
+ begin
+ # Turn off email notifications temporarily
+ Setting.notified_events = []
+ Setting.password_min_length = 4
+ # Run the migration
+ TracMigrate.migrate
+ ensure
+ # Restore previous settings
+ Setting.notified_events = old_notified_events
+ Setting.password_min_length = old_password_min_length
+ end
+ end
+end
+
diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/0c/0ca7bda81025c5f7a9ebad14d8b0c08f3a0b8176.svn-base
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/.svn/pristine/0c/0ca7bda81025c5f7a9ebad14d8b0c08f3a0b8176.svn-base Tue Jan 14 14:37:42 2014 +0000
@@ -0,0 +1,35 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 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 File.expand_path('../../../test_helper', __FILE__)
+
+class RoutingSysTest < ActionController::IntegrationTest
+ def test_sys
+ assert_routing(
+ { :method => 'get', :path => "/sys/projects" },
+ { :controller => 'sys', :action => 'projects' }
+ )
+ assert_routing(
+ { :method => 'post', :path => "/sys/projects/testid/repository" },
+ { :controller => 'sys', :action => 'create_project_repository', :id => 'testid' }
+ )
+ assert_routing(
+ { :method => 'get', :path => "/sys/fetch_changesets" },
+ { :controller => 'sys', :action => 'fetch_changesets' }
+ )
+ end
+end
diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/0c/0cb5ab49f6fd89443044a7c2f37992f0e2874866.svn-base
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/.svn/pristine/0c/0cb5ab49f6fd89443044a7c2f37992f0e2874866.svn-base Tue Jan 14 14:37:42 2014 +0000
@@ -0,0 +1,257 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2007 Patrick Aljord patcito@ŋmail.com
+#
+# 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 'redmine/scm/adapters/git_adapter'
+
+class Repository::Git < Repository
+ attr_protected :root_url
+ validates_presence_of :url
+
+ def self.human_attribute_name(attribute_key_name, *args)
+ attr_name = attribute_key_name.to_s
+ if attr_name == "url"
+ attr_name = "path_to_repository"
+ end
+ super(attr_name, *args)
+ end
+
+ def self.scm_adapter_class
+ Redmine::Scm::Adapters::GitAdapter
+ end
+
+ def self.scm_name
+ 'Git'
+ end
+
+ def report_last_commit
+ extra_report_last_commit
+ end
+
+ def extra_report_last_commit
+ return false if extra_info.nil?
+ v = extra_info["extra_report_last_commit"]
+ return false if v.nil?
+ v.to_s != '0'
+ end
+
+ def supports_directory_revisions?
+ true
+ end
+
+ def supports_revision_graph?
+ true
+ end
+
+ def repo_log_encoding
+ 'UTF-8'
+ end
+
+ # Returns the identifier for the given git changeset
+ def self.changeset_identifier(changeset)
+ changeset.scmid
+ end
+
+ # Returns the readable identifier for the given git changeset
+ def self.format_changeset_identifier(changeset)
+ changeset.revision[0, 8]
+ end
+
+ def branches
+ scm.branches
+ end
+
+ def tags
+ scm.tags
+ end
+
+ def default_branch
+ scm.default_branch
+ rescue Exception => e
+ logger.error "git: error during get default branch: #{e.message}"
+ nil
+ end
+
+ def find_changeset_by_name(name)
+ if name.present?
+ changesets.where(:revision => name.to_s).first ||
+ changesets.where('scmid LIKE ?', "#{name}%").first
+ end
+ end
+
+ def entries(path=nil, identifier=nil)
+ entries = scm.entries(path, identifier, :report_last_commit => extra_report_last_commit)
+ load_entries_changesets(entries)
+ entries
+ end
+
+ # With SCMs that have a sequential commit numbering,
+ # such as Subversion and Mercurial,
+ # Redmine is able to be clever and only fetch changesets
+ # going forward from the most recent one it knows about.
+ #
+ # However, Git does not have a sequential commit numbering.
+ #
+ # In order to fetch only new adding revisions,
+ # Redmine needs to save "heads".
+ #
+ # In Git and Mercurial, revisions are not in date order.
+ # Redmine Mercurial fixed issues.
+ # * Redmine Takes Too Long On Large Mercurial Repository
+ # http://www.redmine.org/issues/3449
+ # * Sorting for changesets might go wrong on Mercurial repos
+ # http://www.redmine.org/issues/3567
+ #
+ # Database revision column is text, so Redmine can not sort by revision.
+ # Mercurial has revision number, and revision number guarantees revision order.
+ # Redmine Mercurial model stored revisions ordered by database id to database.
+ # So, Redmine Mercurial model can use correct ordering revisions.
+ #
+ # Redmine Mercurial adapter uses "hg log -r 0:tip --limit 10"
+ # to get limited revisions from old to new.
+ # But, Git 1.7.3.4 does not support --reverse with -n or --skip.
+ #
+ # The repository can still be fully reloaded by calling #clear_changesets
+ # before fetching changesets (eg. for offline resync)
+ def fetch_changesets
+ scm_brs = branches
+ return if scm_brs.nil? || scm_brs.empty?
+
+ h1 = extra_info || {}
+ h = h1.dup
+ repo_heads = scm_brs.map{ |br| br.scmid }
+ h["heads"] ||= []
+ prev_db_heads = h["heads"].dup
+ if prev_db_heads.empty?
+ prev_db_heads += heads_from_branches_hash
+ end
+ return if prev_db_heads.sort == repo_heads.sort
+
+ h["db_consistent"] ||= {}
+ if changesets.count == 0
+ h["db_consistent"]["ordering"] = 1
+ merge_extra_info(h)
+ self.save
+ elsif ! h["db_consistent"].has_key?("ordering")
+ h["db_consistent"]["ordering"] = 0
+ merge_extra_info(h)
+ self.save
+ end
+ save_revisions(prev_db_heads, repo_heads)
+ end
+
+ def save_revisions(prev_db_heads, repo_heads)
+ h = {}
+ opts = {}
+ opts[:reverse] = true
+ opts[:excludes] = prev_db_heads
+ opts[:includes] = repo_heads
+
+ revisions = scm.revisions('', nil, nil, opts)
+ return if revisions.blank?
+
+ # Make the search for existing revisions in the database in a more sufficient manner
+ #
+ # Git branch is the reference to the specific revision.
+ # Git can *delete* remote branch and *re-push* branch.
+ #
+ # $ git push remote :branch
+ # $ git push remote branch
+ #
+ # After deleting branch, revisions remain in repository until "git gc".
+ # On git 1.7.2.3, default pruning date is 2 weeks.
+ # So, "git log --not deleted_branch_head_revision" return code is 0.
+ #
+ # After re-pushing branch, "git log" returns revisions which are saved in database.
+ # So, Redmine needs to scan revisions and database every time.
+ #
+ # This is replacing the one-after-one queries.
+ # Find all revisions, that are in the database, and then remove them from the revision array.
+ # Then later we won't need any conditions for db existence.
+ # Query for several revisions at once, and remove them from the revisions array, if they are there.
+ # Do this in chunks, to avoid eventual memory problems (in case of tens of thousands of commits).
+ # If there are no revisions (because the original code's algorithm filtered them),
+ # then this part will be stepped over.
+ # We make queries, just if there is any revision.
+ limit = 100
+ offset = 0
+ revisions_copy = revisions.clone # revisions will change
+ while offset < revisions_copy.size
+ scmids = revisions_copy.slice(offset, limit).map{|x| x.scmid}
+ recent_changesets_slice = changesets.where(:scmid => scmids).all
+ # Subtract revisions that redmine already knows about
+ recent_revisions = recent_changesets_slice.map{|c| c.scmid}
+ revisions.reject!{|r| recent_revisions.include?(r.scmid)}
+ offset += limit
+ end
+
+ revisions.each do |rev|
+ transaction do
+ # There is no search in the db for this revision, because above we ensured,
+ # that it's not in the db.
+ save_revision(rev)
+ end
+ end
+ h["heads"] = repo_heads.dup
+ merge_extra_info(h)
+ self.save
+ end
+ private :save_revisions
+
+ def save_revision(rev)
+ parents = (rev.parents || []).collect{|rp| find_changeset_by_name(rp)}.compact
+ changeset = Changeset.create(
+ :repository => self,
+ :revision => rev.identifier,
+ :scmid => rev.scmid,
+ :committer => rev.author,
+ :committed_on => rev.time,
+ :comments => rev.message,
+ :parents => parents
+ )
+ unless changeset.new_record?
+ rev.paths.each { |change| changeset.create_change(change) }
+ end
+ changeset
+ end
+ private :save_revision
+
+ def heads_from_branches_hash
+ h1 = extra_info || {}
+ h = h1.dup
+ h["branches"] ||= {}
+ h['branches'].map{|br, hs| hs['last_scmid']}
+ end
+
+ def latest_changesets(path,rev,limit=10)
+ revisions = scm.revisions(path, nil, rev, :limit => limit, :all => false)
+ return [] if revisions.nil? || revisions.empty?
+
+ changesets.where(:scmid => revisions.map {|c| c.scmid}).all
+ end
+
+ def clear_extra_info_of_changesets
+ return if extra_info.nil?
+ v = extra_info["extra_report_last_commit"]
+ write_attribute(:extra_info, nil)
+ h = {}
+ h["extra_report_last_commit"] = v
+ merge_extra_info(h)
+ self.save
+ end
+ private :clear_extra_info_of_changesets
+end
diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/0c/0cc9601ec1a6e3a27ab4a946d394cf4ba82254b4.svn-base
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/.svn/pristine/0c/0cc9601ec1a6e3a27ab4a946d394cf4ba82254b4.svn-base Tue Jan 14 14:37:42 2014 +0000
@@ -0,0 +1,22 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class TimeEntryActivityCustomField < CustomField
+ def type_name
+ :enumeration_activities
+ end
+end
diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/0d/0d408463a01e894d3f86946e8b5d1925b0335095.svn-base
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/.svn/pristine/0d/0d408463a01e894d3f86946e8b5d1925b0335095.svn-base Tue Jan 14 14:37:42 2014 +0000
@@ -0,0 +1,14 @@
+var fileSpan = $('#attachments_<%= j params[:attachment_id] %>');
+<% if @attachment.new_record? %>
+ fileSpan.hide();
+ alert("<%= escape_javascript @attachment.errors.full_messages.join(', ') %>");
+<% else %>
+$('', { type: 'hidden', name: 'attachments[<%= j params[:attachment_id] %>][token]' } ).val('<%= j @attachment.token %>').appendTo(fileSpan);
+fileSpan.find('a.remove-upload')
+ .attr({
+ "data-remote": true,
+ "data-method": 'delete',
+ href: '<%= j attachment_path(@attachment, :attachment_id => params[:attachment_id], :format => 'js') %>'
+ })
+ .off('click');
+<% end %>
diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/0d/0d422b89acdac2bb8c5929f11e22f0904e9f370b.svn-base
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/.svn/pristine/0d/0d422b89acdac2bb8c5929f11e22f0904e9f370b.svn-base Tue Jan 14 14:37:42 2014 +0000
@@ -0,0 +1,74 @@
+api.issue do
+ api.id @issue.id
+ api.project(:id => @issue.project_id, :name => @issue.project.name) unless @issue.project.nil?
+ api.tracker(:id => @issue.tracker_id, :name => @issue.tracker.name) unless @issue.tracker.nil?
+ api.status(:id => @issue.status_id, :name => @issue.status.name) unless @issue.status.nil?
+ api.priority(:id => @issue.priority_id, :name => @issue.priority.name) unless @issue.priority.nil?
+ api.author(:id => @issue.author_id, :name => @issue.author.name) unless @issue.author.nil?
+ api.assigned_to(:id => @issue.assigned_to_id, :name => @issue.assigned_to.name) unless @issue.assigned_to.nil?
+ api.category(:id => @issue.category_id, :name => @issue.category.name) unless @issue.category.nil?
+ api.fixed_version(:id => @issue.fixed_version_id, :name => @issue.fixed_version.name) unless @issue.fixed_version.nil?
+ api.parent(:id => @issue.parent_id) unless @issue.parent.nil?
+
+ api.subject @issue.subject
+ api.description @issue.description
+ api.start_date @issue.start_date
+ api.due_date @issue.due_date
+ api.done_ratio @issue.done_ratio
+ api.estimated_hours @issue.estimated_hours
+ api.spent_hours(@issue.spent_hours) if User.current.allowed_to?(:view_time_entries, @project)
+
+ render_api_custom_values @issue.visible_custom_field_values, api
+
+ api.created_on @issue.created_on
+ api.updated_on @issue.updated_on
+ api.closed_on @issue.closed_on
+
+ render_api_issue_children(@issue, api) if include_in_api_response?('children')
+
+ api.array :attachments do
+ @issue.attachments.each do |attachment|
+ render_api_attachment(attachment, api)
+ end
+ end if include_in_api_response?('attachments')
+
+ api.array :relations do
+ @relations.each do |relation|
+ api.relation(:id => relation.id, :issue_id => relation.issue_from_id, :issue_to_id => relation.issue_to_id, :relation_type => relation.relation_type, :delay => relation.delay)
+ end
+ end if include_in_api_response?('relations') && @relations.present?
+
+ api.array :changesets do
+ @issue.changesets.each do |changeset|
+ api.changeset :revision => changeset.revision do
+ api.user(:id => changeset.user_id, :name => changeset.user.name) unless changeset.user.nil?
+ api.comments changeset.comments
+ api.committed_on changeset.committed_on
+ end
+ end
+ end if include_in_api_response?('changesets') && User.current.allowed_to?(:view_changesets, @project)
+
+ api.array :journals do
+ @journals.each do |journal|
+ api.journal :id => journal.id do
+ api.user(:id => journal.user_id, :name => journal.user.name) unless journal.user.nil?
+ api.notes journal.notes
+ api.created_on journal.created_on
+ api.array :details do
+ journal.visible_details.each do |detail|
+ api.detail :property => detail.property, :name => detail.prop_key do
+ api.old_value detail.old_value
+ api.new_value detail.value
+ end
+ end
+ end
+ end
+ end
+ end if include_in_api_response?('journals')
+
+ api.array :watchers do
+ @issue.watcher_users.each do |user|
+ api.user :id => user.id, :name => user.name
+ end
+ end if include_in_api_response?('watchers') && User.current.allowed_to?(:view_issue_watchers, @issue.project)
+end
diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/0d/0d50a39088b6bd107e54b315b0bf77a7ffa63596.svn-base
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/.svn/pristine/0d/0d50a39088b6bd107e54b315b0bf77a7ffa63596.svn-base Tue Jan 14 14:37:42 2014 +0000
@@ -0,0 +1,110 @@
+### From http://svn.geekdaily.org/public/rails/plugins/generally_useful/tasks/coverage_via_rcov.rake
+
+namespace :test do
+ desc 'Measures test coverage'
+ task :coverage do
+ rm_f "coverage"
+ rm_f "coverage.data"
+ rcov = "rcov --rails --aggregate coverage.data --text-summary -Ilib --html --exclude gems/"
+ files = %w(unit functional integration).map {|dir| Dir.glob("test/#{dir}/**/*_test.rb")}.flatten.join(" ")
+ system("#{rcov} #{files}")
+ end
+
+ desc 'Run unit and functional scm tests'
+ task :scm do
+ errors = %w(test:scm:units test:scm:functionals).collect do |task|
+ begin
+ Rake::Task[task].invoke
+ nil
+ rescue => e
+ task
+ end
+ end.compact
+ abort "Errors running #{errors.to_sentence(:locale => :en)}!" if errors.any?
+ end
+
+ namespace :scm do
+ namespace :setup do
+ desc "Creates directory for test repositories"
+ task :create_dir do
+ FileUtils.mkdir_p Rails.root + '/tmp/test'
+ end
+
+ supported_scms = [:subversion, :cvs, :bazaar, :mercurial, :git, :darcs, :filesystem]
+
+ desc "Creates a test subversion repository"
+ task :subversion => :create_dir do
+ repo_path = "tmp/test/subversion_repository"
+ unless File.exists?(repo_path)
+ system "svnadmin create #{repo_path}"
+ system "gunzip < test/fixtures/repositories/subversion_repository.dump.gz | svnadmin load #{repo_path}"
+ end
+ end
+
+ desc "Creates a test mercurial repository"
+ task :mercurial => :create_dir do
+ repo_path = "tmp/test/mercurial_repository"
+ unless File.exists?(repo_path)
+ bundle_path = "test/fixtures/repositories/mercurial_repository.hg"
+ system "hg init #{repo_path}"
+ system "hg -R #{repo_path} pull #{bundle_path}"
+ end
+ end
+
+ (supported_scms - [:subversion, :mercurial]).each do |scm|
+ desc "Creates a test #{scm} repository"
+ task scm => :create_dir do
+ unless File.exists?("tmp/test/#{scm}_repository")
+ # system "gunzip < test/fixtures/repositories/#{scm}_repository.tar.gz | tar -xv -C tmp/test"
+ system "tar -xvz -C tmp/test -f test/fixtures/repositories/#{scm}_repository.tar.gz"
+ end
+ end
+ end
+
+ desc "Creates all test repositories"
+ task :all => supported_scms
+ end
+
+ desc "Updates installed test repositories"
+ task :update do
+ require 'fileutils'
+ Dir.glob("tmp/test/*_repository").each do |dir|
+ next unless File.basename(dir) =~ %r{^(.+)_repository$} && File.directory?(dir)
+ scm = $1
+ next unless fixture = Dir.glob("test/fixtures/repositories/#{scm}_repository.*").first
+ next if File.stat(dir).ctime > File.stat(fixture).mtime
+
+ FileUtils.rm_rf dir
+ Rake::Task["test:scm:setup:#{scm}"].execute
+ end
+ end
+
+ Rake::TestTask.new(:units => "db:test:prepare") do |t|
+ t.libs << "test"
+ t.verbose = true
+ t.test_files = FileList['test/unit/repository*_test.rb'] + FileList['test/unit/lib/redmine/scm/**/*_test.rb']
+ end
+ Rake::Task['test:scm:units'].comment = "Run the scm unit tests"
+
+ Rake::TestTask.new(:functionals => "db:test:prepare") do |t|
+ t.libs << "test"
+ t.verbose = true
+ t.test_files = FileList['test/functional/repositories*_test.rb']
+ end
+ Rake::Task['test:scm:functionals'].comment = "Run the scm functional tests"
+ end
+
+ Rake::TestTask.new(:rdm_routing) do |t|
+ t.libs << "test"
+ t.verbose = true
+ t.test_files = FileList['test/integration/routing/*_test.rb']
+ end
+ Rake::Task['test:rdm_routing'].comment = "Run the routing tests"
+
+ Rake::TestTask.new(:ui => "db:test:prepare") do |t|
+ t.libs << "test"
+ t.verbose = true
+ t.test_files = FileList['test/ui/**/*_test.rb']
+ end
+ Rake::Task['test:ui'].comment = "Run the UI tests with Capybara (PhantomJS listening on port 4444 is required)"
+end
diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/0d/0d6f6e567915dec9c4fe1362362e16f727a01572.svn-base
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/.svn/pristine/0d/0d6f6e567915dec9c4fe1362362e16f727a01572.svn-base Tue Jan 14 14:37:42 2014 +0000
@@ -0,0 +1,38 @@
+<%= l(:label_board) %> | +<%= l(:label_topic_plural) %> | +<%= l(:label_message_plural) %> | +<%= l(:label_message_last) %> | +
---|---|---|---|
+ <%= link_to h(board.name), project_board_path(board.project, board), :class => "board" %> + <%=h board.description %> + |
+ <%= board.topics_count %> | + + +
<%= submit_tag l(:label_logout) %>
+<% end %> diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/0e/0ed2a8155e8187c04d224caafbc3970f5a91d733.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0e/0ed2a8155e8187c04d224caafbc3970f5a91d733.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,198 @@ ++ <% if @copy %> + <%= hidden_field_tag 'copy', '1' %> + <%= submit_tag l(:button_copy) %> + <%= submit_tag l(:button_copy_and_follow), :name => 'follow' %> + <% elsif @target_project %> + <%= submit_tag l(:button_move) %> + <%= submit_tag l(:button_move_and_follow), :name => 'follow' %> + <% else %> + <%= submit_tag l(:button_submit) %> + <% end %> +
+ +<% end %> + +<%= javascript_tag do %> +$(window).load(function(){ + $(document).on('change', 'input[data-disables]', function(){ + if ($(this).attr('checked')){ + $($(this).data('disables')).attr('disabled', true).val(''); + } else { + $($(this).data('disables')).attr('disabled', false); + } + }); +}); +$(document).ready(function(){ + $('input[data-disables]').trigger('change'); +}); +<% end %> diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/0e/0efedaea2e714f5dbe3b6a1b0cd102c208875d70.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0e/0efedaea2e714f5dbe3b6a1b0cd102c208875d70.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,1104 @@ +# Chinese (China) translations for Ruby on Rails +# by tsechingho (http://github.com/tsechingho) +zh: + # Text direction: Left-to-Right (ltr) or Right-to-Left (rtl) + direction: ltr + jquery: + locale: "zh-CN" + date: + formats: + # Use the strftime parameters for formats. + # When no format has been given, it uses default. + # You can provide other formats here if you like! + default: "%Y-%m-%d" + short: "%b%dæ—¥" + long: "%Yå¹´%b%dæ—¥" + + day_names: [星期天, 星期一, 星期二, 星期三, 星期四, 星期五, 星期å…] + abbr_day_names: [æ—¥, 一, 二, 三, å››, 五, å…] + + # Don't forget the nil at the beginning; there's no such thing as a 0th month + month_names: [~, 一月, 二月, 三月, 四月, 五月, å…æœˆ, 七月, 八月, 乿œˆ, åæœˆ, å一月, å二月] + abbr_month_names: [~, 1月, 2月, 3月, 4月, 5月, 6月, 7月, 8月, 9月, 10月, 11月, 12月] + # Used in date_select and datime_select. + order: + - :year + - :month + - :day + + time: + formats: + default: "%Yå¹´%b%dæ—¥ %A %H:%M:%S" + time: "%H:%M" + short: "%b%dæ—¥ %H:%M" + long: "%Yå¹´%b%dæ—¥ %H:%M" + am: "上åˆ" + pm: "下åˆ" + + datetime: + distance_in_words: + half_a_minute: "åŠåˆ†é’Ÿ" + less_than_x_seconds: + one: "一秒内" + other: "少于 %{count} ç§’" + x_seconds: + one: "一秒" + other: "%{count} ç§’" + less_than_x_minutes: + one: "一分钟内" + other: "少于 %{count} 分钟" + x_minutes: + one: "一分钟" + other: "%{count} 分钟" + about_x_hours: + one: "å¤§çº¦ä¸€å°æ—¶" + other: "大约 %{count} å°æ—¶" + x_hours: + one: "1 å°æ—¶" + other: "%{count} å°æ—¶" + x_days: + one: "一天" + other: "%{count} 天" + about_x_months: + one: "大约一个月" + other: "大约 %{count} 个月" + x_months: + one: "一个月" + other: "%{count} 个月" + about_x_years: + one: "大约一年" + other: "大约 %{count} å¹´" + over_x_years: + one: "超过一年" + other: "超过 %{count} å¹´" + almost_x_years: + one: "将近 1 å¹´" + other: "将近 %{count} å¹´" + + number: + # Default format for numbers + format: + separator: "." + delimiter: "" + precision: 3 + human: + format: + delimiter: "" + precision: 3 + storage_units: + format: "%n %u" + units: + byte: + one: "Byte" + other: "Bytes" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + +# Used in array.to_sentence. + support: + array: + sentence_connector: "å’Œ" + skip_last_comma: false + + activerecord: + errors: + template: + header: + one: "由于å‘生了一个错误 %{model} æ— æ³•ä¿å˜" + other: "%{count} 个错误使得 %{model} æ— æ³•ä¿å˜" + messages: + inclusion: "ä¸åŒ…å«äºŽåˆ—表ä¸" + exclusion: "是ä¿ç•™å…³é”®å—" + invalid: "æ˜¯æ— æ•ˆçš„" + confirmation: "与确认值ä¸åŒ¹é…" + accepted: "必须是å¯è¢«æŽ¥å—çš„" + empty: "ä¸èƒ½ç•™ç©º" + blank: "ä¸èƒ½ä¸ºç©ºå—符" + too_long: "过长(最长为 %{count} 个å—符)" + too_short: "过çŸï¼ˆæœ€çŸä¸º %{count} 个å—符)" + wrong_length: "é•¿åº¦éžæ³•(必须为 %{count} 个å—符)" + taken: "å·²ç»è¢«ä½¿ç”¨" + not_a_number: "䏿˜¯æ•°å—" + not_a_date: "䏿˜¯åˆæ³•日期" + greater_than: "必须大于 %{count}" + greater_than_or_equal_to: "必须大于或ç‰äºŽ %{count}" + equal_to: "å¿…é¡»ç‰äºŽ %{count}" + less_than: "å¿…é¡»å°äºŽ %{count}" + less_than_or_equal_to: "å¿…é¡»å°äºŽæˆ–ç‰äºŽ %{count}" + odd: "å¿…é¡»ä¸ºå•æ•°" + even: "å¿…é¡»ä¸ºåŒæ•°" + greater_than_start_date: "必须在起始日期之åŽ" + not_same_project: "ä¸å±žäºŽåŒä¸€ä¸ªé¡¹ç›®" + circular_dependency: "æ¤å…³è”将导致循环ä¾èµ–" + cant_link_an_issue_with_a_descendant: "问题ä¸èƒ½å…³è”到它的å任务" + earlier_than_minimum_start_date: "cannot be earlier than %{date} because of preceding issues" + + actionview_instancetag_blank_option: 请选择 + + general_text_No: 'å¦' + general_text_Yes: '是' + general_text_no: 'å¦' + general_text_yes: '是' + general_lang_name: 'Simplified Chinese (ç®€ä½“ä¸æ–‡)' + general_csv_separator: ',' + general_csv_decimal_separator: '.' + general_csv_encoding: gb18030 + general_pdf_encoding: gb18030 + general_first_day_of_week: '7' + + notice_account_updated: å¸å·æ›´æ–°æˆåŠŸ + notice_account_invalid_creditentials: æ— æ•ˆçš„ç”¨æˆ·åæˆ–密ç + notice_account_password_updated: å¯†ç æ›´æ–°æˆåŠŸ + notice_account_wrong_password: 密ç 错误 + notice_account_register_done: å¸å·åˆ›å»ºæˆåŠŸï¼Œè¯·ä½¿ç”¨æ³¨å†Œç¡®è®¤é‚®ä»¶ä¸çš„é“¾æŽ¥æ¥æ¿€æ´»æ‚¨çš„å¸å·ã€‚ + notice_account_unknown_email: 未知用户 + notice_can_t_change_password: 该å¸å·ä½¿ç”¨äº†å¤–部认è¯ï¼Œå› æ¤æ— 法更改密ç 。 + notice_account_lost_email_sent: 系统已将引导您设置新密ç 的邮件å‘é€ç»™æ‚¨ã€‚ + notice_account_activated: 您的å¸å·å·²è¢«æ¿€æ´»ã€‚您现在å¯ä»¥ç™»å½•了。 + notice_successful_create: 创建æˆåŠŸ + notice_successful_update: æ›´æ–°æˆåŠŸ + notice_successful_delete: åˆ é™¤æˆåŠŸ + notice_successful_connection: 连接æˆåŠŸ + notice_file_not_found: 您访问的页é¢ä¸å˜åœ¨æˆ–å·²è¢«åˆ é™¤ã€‚ + notice_locking_conflict: æ•°æ®å·²è¢«å¦ä¸€ä½ç”¨æˆ·æ›´æ–° + notice_not_authorized: 对ä¸èµ·ï¼Œæ‚¨æ— æƒè®¿é—®æ¤é¡µé¢ã€‚ + notice_not_authorized_archived_project: è¦è®¿é—®çš„项目已ç»å½’档。 + notice_email_sent: "邮件已å‘é€è‡³ %{value}" + notice_email_error: "å‘é€é‚®ä»¶æ—¶å‘生错误 (%{value})" + notice_feeds_access_key_reseted: 您的Atomå˜å–键已被é‡ç½®ã€‚ + notice_api_access_key_reseted: 您的API访问键已被é‡ç½®ã€‚ + notice_failed_to_save_issues: "%{count} 个问题ä¿å˜å¤±è´¥ï¼ˆå…±é€‰æ‹© %{total} 个问题):%{ids}." + notice_failed_to_save_members: "æˆå‘˜ä¿å˜å¤±è´¥: %{errors}." + notice_no_issue_selected: "未选择任何问题ï¼è¯·é€‰æ‹©æ‚¨è¦ç¼–辑的问题。" + notice_account_pending: "您的å¸å·å·²è¢«æˆåŠŸåˆ›å»ºï¼Œæ£åœ¨ç‰å¾…管ç†å‘˜çš„å®¡æ ¸ã€‚" + notice_default_data_loaded: æˆåŠŸè½½å…¥é»˜è®¤è®¾ç½®ã€‚ + notice_unable_delete_version: æ— æ³•åˆ é™¤ç‰ˆæœ¬ + notice_unable_delete_time_entry: æ— æ³•åˆ é™¤å·¥æ—¶ + notice_issue_done_ratios_updated: 问题完æˆåº¦å·²æ›´æ–°ã€‚ + notice_gantt_chart_truncated: "The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max})" + + error_can_t_load_default_data: "æ— æ³•è½½å…¥é»˜è®¤è®¾ç½®ï¼š%{value}" + error_scm_not_found: "版本库ä¸ä¸å˜åœ¨è¯¥æ¡ç›®å’Œï¼ˆæˆ–)其修订版本。" + error_scm_command_failed: "访问版本库时å‘生错误:%{value}" + error_scm_annotate: "该æ¡ç›®ä¸å˜åœ¨æˆ–æ— æ³•è¿½æº¯ã€‚" + error_issue_not_found_in_project: '问题ä¸å˜åœ¨æˆ–ä¸å±žäºŽæ¤é¡¹ç›®' + error_no_tracker_in_project: è¯¥é¡¹ç›®æœªè®¾å®šè·Ÿè¸ªæ ‡ç¾ï¼Œè¯·æ£€æŸ¥é¡¹ç›®é…置。 + error_no_default_issue_status: 未设置默认的问题状æ€ã€‚请检查系统设置("管ç†" -> "问题状æ€")。 + error_can_not_delete_custom_field: æ— æ³•åˆ é™¤è‡ªå®šä¹‰å±žæ€§ + error_can_not_delete_tracker: "è¯¥è·Ÿè¸ªæ ‡ç¾å·²åŒ…å«é—®é¢˜,æ— æ³•åˆ é™¤" + error_can_not_remove_role: "该角色æ£åœ¨ä½¿ç”¨ä¸ï¼Œæ— æ³•åˆ é™¤" + error_can_not_reopen_issue_on_closed_version: 该问题被关è”到一个已ç»å…³é—çš„ç‰ˆæœ¬ï¼Œå› æ¤æ— æ³•é‡æ–°æ‰“开。 + error_can_not_archive_project: è¯¥é¡¹ç›®æ— æ³•è¢«å˜æ¡£ + error_issue_done_ratios_not_updated: 问题完æˆåº¦æœªèƒ½è¢«æ›´æ–°ã€‚ + error_workflow_copy_source: 请选择一个æºè·Ÿè¸ªæ ‡ç¾æˆ–者角色 + error_workflow_copy_target: è¯·é€‰æ‹©ç›®æ ‡è·Ÿè¸ªæ ‡ç¾å’Œè§’色 + error_unable_delete_issue_status: 'æ— æ³•åˆ é™¤é—®é¢˜çŠ¶æ€' + error_unable_to_connect: "æ— æ³•è¿žæŽ¥ (%{value})" + warning_attachments_not_saved: "%{count} 个文件ä¿å˜å¤±è´¥" + + mail_subject_lost_password: "您的 %{value} 密ç " + mail_body_lost_password: '请点击以下链接æ¥ä¿®æ”¹æ‚¨çš„密ç :' + mail_subject_register: "%{value}å¸å·æ¿€æ´»" + mail_body_register: 'è¯·ç‚¹å‡»ä»¥ä¸‹é“¾æŽ¥æ¥æ¿€æ´»æ‚¨çš„å¸å·ï¼š' + mail_body_account_information_external: "您å¯ä»¥ä½¿ç”¨æ‚¨çš„ %{value} å¸å·æ¥ç™»å½•。" + mail_body_account_information: 您的å¸å·ä¿¡æ¯ + mail_subject_account_activation_request: "%{value}å¸å·æ¿€æ´»è¯·æ±‚" + mail_body_account_activation_request: "新用户(%{value}ï¼‰å·²å®Œæˆæ³¨å†Œï¼Œæ£åœ¨ç‰å€™æ‚¨çš„å®¡æ ¸ï¼š" + mail_subject_reminder: "%{count} 个问题需è¦å°½å¿«è§£å†³ (%{days})" + mail_body_reminder: "指派给您的 %{count} 个问题需è¦åœ¨ %{days} 天内完æˆï¼š" + mail_subject_wiki_content_added: "'%{id}' wiki页é¢å·²æ·»åŠ " + mail_body_wiki_content_added: "'%{id}' wiki页é¢å·²ç”± %{author} æ·»åŠ ã€‚" + mail_subject_wiki_content_updated: "'%{id}' wiki页é¢å·²æ›´æ–°ã€‚" + mail_body_wiki_content_updated: "'%{id}' wiki页é¢å·²ç”± %{author} 更新。" + + + field_name: åç§° + field_description: æè¿° + field_summary: æ‘˜è¦ + field_is_required: å¿…å¡« + field_firstname: åå— + field_lastname: å§“æ° + field_mail: é‚®ä»¶åœ°å€ + field_filename: 文件 + field_filesize: å¤§å° + field_downloads: 下载次数 + field_author: 作者 + field_created_on: 创建于 + field_updated_on: 更新于 + field_field_format: æ ¼å¼ + field_is_for_all: 用于所有项目 + field_possible_values: å¯èƒ½çš„值 + field_regexp: æ£åˆ™è¡¨è¾¾å¼ + field_min_length: 最å°é•¿åº¦ + field_max_length: 最大长度 + field_value: 值 + field_category: 类别 + field_title: æ ‡é¢˜ + field_project: 项目 + field_issue: 问题 + field_status: çŠ¶æ€ + field_notes: 说明 + field_is_closed: 已关é—的问题 + field_is_default: 默认值 + field_tracker: 跟踪 + field_subject: 主题 + field_due_date: è®¡åˆ’å®Œæˆæ—¥æœŸ + field_assigned_to: 指派给 + field_priority: 优先级 + field_fixed_version: ç›®æ ‡ç‰ˆæœ¬ + field_user: 用户 + field_principal: 用户/用户组 + field_role: 角色 + field_homepage: 主页 + field_is_public: 公开 + field_parent: 上级项目 + field_is_in_roadmap: åœ¨è·¯çº¿å›¾ä¸æ˜¾ç¤º + field_login: 登录å + field_mail_notification: 邮件通知 + field_admin: 管ç†å‘˜ + field_last_login_on: 最åŽç™»å½• + field_language: è¯è¨€ + field_effective_date: 日期 + field_password: 密ç + field_new_password: 新密ç + field_password_confirmation: 确认 + field_version: 版本 + field_type: 类型 + field_host: 主机 + field_port: ç«¯å£ + field_account: å¸å· + field_base_dn: Base DN + field_attr_login: 登录å属性 + field_attr_firstname: åå—属性 + field_attr_lastname: å§“æ°å±žæ€§ + field_attr_mail: 邮件属性 + field_onthefly: 峿—¶ç”¨æˆ·ç”Ÿæˆ + field_start_date: 开始日期 + field_done_ratio: "% 完æˆ" + field_auth_source: è®¤è¯æ¨¡å¼ + field_hide_mail: éšè—æˆ‘çš„é‚®ä»¶åœ°å€ + field_comments: 注释 + field_url: URL + field_start_page: 起始页 + field_subproject: å项目 + field_hours: å°æ—¶ + field_activity: 活动 + field_spent_on: 日期 + field_identifier: æ ‡è¯† + field_is_filter: 作为过滤æ¡ä»¶ + field_issue_to: 相关问题 + field_delay: 延期 + field_assignable: é—®é¢˜å¯æŒ‡æ´¾ç»™æ¤è§’色 + field_redirect_existing_links: é‡å®šå‘到现有链接 + field_estimated_hours: 预期时间 + field_column_names: 列 + field_time_entries: 工时 + field_time_zone: 时区 + field_searchable: å¯ç”¨ä½œæœç´¢æ¡ä»¶ + field_default_value: 默认值 + field_comments_sorting: 显示注释 + field_parent_title: ä¸Šçº§é¡µé¢ + field_editable: å¯ç¼–辑 + field_watcher: 跟踪者 + field_identity_url: OpenID URL + field_content: 内容 + field_group_by: æ ¹æ®æ¤æ¡ä»¶åˆ†ç»„ + field_sharing: 共享 + field_parent_issue: 父任务 + field_member_of_group: 用户组的æˆå‘˜ + field_assigned_to_role: 角色的æˆå‘˜ + field_text: æ–‡æœ¬å—æ®µ + field_visible: å¯è§çš„ + + setting_app_title: åº”ç”¨ç¨‹åºæ ‡é¢˜ + setting_app_subtitle: 应用程åºåæ ‡é¢˜ + setting_welcome_text: æ¬¢è¿Žæ–‡å— + setting_default_language: 默认è¯è¨€ + setting_login_required: è¦æ±‚è®¤è¯ + setting_self_registration: å…许自注册 + setting_attachment_max_size: 附件大å°é™åˆ¶ + setting_issues_export_limit: 问题导出æ¡ç›®çš„é™åˆ¶ + setting_mail_from: 邮件å‘ä»¶äººåœ°å€ + setting_bcc_recipients: ä½¿ç”¨å¯†ä»¶æŠ„é€ (bcc) + setting_plain_text_mail: çº¯æ–‡æœ¬ï¼ˆæ— HTML) + setting_host_name: 主机åç§° + setting_text_formatting: æ–‡æœ¬æ ¼å¼ + setting_wiki_compression: 压缩WikiåŽ†å²æ–‡æ¡£ + setting_feeds_limit: Atom Feedå†…å®¹æ¡æ•°é™åˆ¶ + setting_default_projects_public: 新建项目默认为公开项目 + setting_autofetch_changesets: 自动获å–程åºå˜æ›´ + setting_sys_api_enabled: å¯ç”¨ç”¨äºŽç‰ˆæœ¬åº“管ç†çš„Web Service + setting_commit_ref_keywords: ç”¨äºŽå¼•ç”¨é—®é¢˜çš„å…³é”®å— + setting_commit_fix_keywords: ç”¨äºŽè§£å†³é—®é¢˜çš„å…³é”®å— + setting_autologin: 自动登录 + setting_date_format: æ—¥æœŸæ ¼å¼ + setting_time_format: æ—¶é—´æ ¼å¼ + setting_cross_project_issue_relations: å…许ä¸åŒé¡¹ç›®ä¹‹é—´çš„é—®é¢˜å…³è” + setting_issue_list_default_columns: é—®é¢˜åˆ—è¡¨ä¸æ˜¾ç¤ºçš„默认列 + setting_emails_header: 邮件头 + setting_emails_footer: 邮件ç¾å + setting_protocol: åè®® + setting_per_page_options: æ¯é¡µæ˜¾ç¤ºæ¡ç›®ä¸ªæ•°çš„设置 + setting_user_format: ç”¨æˆ·æ˜¾ç¤ºæ ¼å¼ + setting_activity_days_default: åœ¨é¡¹ç›®æ´»åŠ¨ä¸æ˜¾ç¤ºçš„天数 + setting_display_subprojects_issues: 在项目页é¢ä¸Šé»˜è®¤æ˜¾ç¤ºå项目的问题 + setting_enabled_scm: å¯ç”¨ SCM + setting_mail_handler_body_delimiters: åœ¨è¿™äº›è¡Œä¹‹åŽæˆªæ–邮件 + setting_mail_handler_api_enabled: å¯ç”¨ç”¨äºŽæŽ¥æ”¶é‚®ä»¶çš„æœåŠ¡ + setting_mail_handler_api_key: API key + setting_sequential_project_identifiers: 顺åºäº§ç”Ÿé¡¹ç›®æ ‡è¯† + setting_gravatar_enabled: 使用Gravatarç”¨æˆ·å¤´åƒ + setting_gravatar_default: 默认的Gravatarå¤´åƒ + setting_diff_max_lines_displayed: 查看差别页é¢ä¸Šæ˜¾ç¤ºçš„æœ€å¤§è¡Œæ•° + setting_file_max_size_displayed: å…许直接显示的最大文本文件 + setting_repository_log_display_limit: åœ¨æ–‡ä»¶å˜æ›´è®°å½•页é¢ä¸Šæ˜¾ç¤ºçš„æœ€å¤§ä¿®è®¢ç‰ˆæœ¬æ•°é‡ + setting_openid: å…许使用OpenID登录和注册 + setting_password_min_length: 最çŸå¯†ç 长度 + setting_new_project_user_role_id: éžç®¡ç†å‘˜ç”¨æˆ·æ–°å»ºé¡¹ç›®æ—¶å°†è¢«èµ‹äºˆçš„(在该项目ä¸çš„)角色 + setting_default_projects_modules: 新建项目默认å¯ç”¨çš„æ¨¡å— + setting_issue_done_ratio: 计算问题完æˆåº¦ï¼š + setting_issue_done_ratio_issue_field: 使用问题(的完æˆåº¦ï¼‰å±žæ€§ + setting_issue_done_ratio_issue_status: ä½¿ç”¨é—®é¢˜çŠ¶æ€ + setting_start_of_week: 日历开始于 + setting_rest_api_enabled: å¯ç”¨REST web service + setting_cache_formatted_text: ç¼“å˜æ ¼å¼åŒ–æ–‡å— + setting_default_notification_option: 默认æé†’选项 + setting_commit_logtime_enabled: 激活时间日志 + setting_commit_logtime_activity_id: 记录的活动 + setting_gantt_items_limit: 在甘特图上显示的最大记录数 + + permission_add_project: 新建项目 + permission_add_subprojects: 新建å项目 + permission_edit_project: 编辑项目 + permission_select_project_modules: é€‰æ‹©é¡¹ç›®æ¨¡å— + permission_manage_members: ç®¡ç†æˆå‘˜ + permission_manage_project_activities: 管ç†é¡¹ç›®æ´»åЍ + permission_manage_versions: 管ç†ç‰ˆæœ¬ + permission_manage_categories: 管ç†é—®é¢˜ç±»åˆ« + permission_view_issues: 查看问题 + permission_add_issues: 新建问题 + permission_edit_issues: 更新问题 + permission_manage_issue_relations: 管ç†é—®é¢˜å…³è” + permission_add_issue_notes: æ·»åŠ è¯´æ˜Ž + permission_edit_issue_notes: 编辑说明 + permission_edit_own_issue_notes: 编辑自己的说明 + permission_move_issues: 移动问题 + permission_delete_issues: åˆ é™¤é—®é¢˜ + permission_manage_public_queries: 管ç†å…¬å¼€çš„æŸ¥è¯¢ + permission_save_queries: ä¿å˜æŸ¥è¯¢ + permission_view_gantt: 查看甘特图 + permission_view_calendar: 查看日历 + permission_view_issue_watchers: 查看跟踪者列表 + permission_add_issue_watchers: æ·»åŠ è·Ÿè¸ªè€… + permission_delete_issue_watchers: åˆ é™¤è·Ÿè¸ªè€… + permission_log_time: 登记工时 + permission_view_time_entries: 查看耗时 + permission_edit_time_entries: 编辑耗时 + permission_edit_own_time_entries: 编辑自己的耗时 + permission_manage_news: ç®¡ç†æ–°é—» + permission_comment_news: ä¸ºæ–°é—»æ·»åŠ è¯„è®º + permission_view_documents: 查看文档 + permission_manage_files: ç®¡ç†æ–‡ä»¶ + permission_view_files: 查看文件 + permission_manage_wiki: 管ç†Wiki + permission_rename_wiki_pages: é‡å®šå‘/é‡å‘½åWikié¡µé¢ + permission_delete_wiki_pages: åˆ é™¤Wikié¡µé¢ + permission_view_wiki_pages: 查看Wiki + permission_view_wiki_edits: 查看Wiki历å²è®°å½• + permission_edit_wiki_pages: 编辑Wikié¡µé¢ + permission_delete_wiki_pages_attachments: åˆ é™¤é™„ä»¶ + permission_protect_wiki_pages: ä¿æŠ¤Wikié¡µé¢ + permission_manage_repository: 管ç†ç‰ˆæœ¬åº“ + permission_browse_repository: æµè§ˆç‰ˆæœ¬åº“ + permission_view_changesets: æŸ¥çœ‹å˜æ›´ + permission_commit_access: 访问æäº¤ä¿¡æ¯ + permission_manage_boards: 管ç†è®¨è®ºåŒº + permission_view_messages: 查看帖å + permission_add_messages: å‘表帖å + permission_edit_messages: 编辑帖å + permission_edit_own_messages: 编辑自己的帖å + permission_delete_messages: åˆ é™¤å¸–å + permission_delete_own_messages: åˆ é™¤è‡ªå·±çš„å¸–å + permission_export_wiki_pages: 导出 wiki é¡µé¢ + permission_manage_subtasks: 管ç†å任务 + + project_module_issue_tracking: 问题跟踪 + project_module_time_tracking: 时间跟踪 + project_module_news: æ–°é—» + project_module_documents: 文档 + project_module_files: 文件 + project_module_wiki: Wiki + project_module_repository: 版本库 + project_module_boards: 讨论区 + project_module_calendar: 日历 + project_module_gantt: 甘特图 + + label_user: 用户 + label_user_plural: 用户 + label_user_new: 新建用户 + label_user_anonymous: 匿å用户 + label_project: 项目 + label_project_new: 新建项目 + label_project_plural: 项目 + label_x_projects: + zero: æ— é¡¹ç›® + one: 1 个项目 + other: "%{count} 个项目" + label_project_all: 所有的项目 + label_project_latest: 最近的项目 + label_issue: 问题 + label_issue_new: 新建问题 + label_issue_plural: 问题 + label_issue_view_all: 查看所有问题 + label_issues_by: "按 %{value} 分组显示问题" + label_issue_added: é—®é¢˜å·²æ·»åŠ + label_issue_updated: 问题已更新 + label_document: 文档 + label_document_new: 新建文档 + label_document_plural: 文档 + label_document_added: æ–‡æ¡£å·²æ·»åŠ + label_role: 角色 + label_role_plural: 角色 + label_role_new: 新建角色 + label_role_and_permissions: 角色和æƒé™ + label_member: æˆå‘˜ + label_member_new: 新建æˆå‘˜ + label_member_plural: æˆå‘˜ + label_tracker: è·Ÿè¸ªæ ‡ç¾ + label_tracker_plural: è·Ÿè¸ªæ ‡ç¾ + label_tracker_new: æ–°å»ºè·Ÿè¸ªæ ‡ç¾ + label_workflow: 工作æµç¨‹ + label_issue_status: é—®é¢˜çŠ¶æ€ + label_issue_status_plural: é—®é¢˜çŠ¶æ€ + label_issue_status_new: æ–°å»ºé—®é¢˜çŠ¶æ€ + label_issue_category: 问题类别 + label_issue_category_plural: 问题类别 + label_issue_category_new: 新建问题类别 + label_custom_field: 自定义属性 + label_custom_field_plural: 自定义属性 + label_custom_field_new: 新建自定义属性 + label_enumerations: 枚举值 + label_enumeration_new: 新建枚举值 + label_information: ä¿¡æ¯ + label_information_plural: ä¿¡æ¯ + label_please_login: 请登录 + label_register: 注册 + label_login_with_open_id_option: 或使用OpenID登录 + label_password_lost: 忘记密ç + label_home: 主页 + label_my_page: æˆ‘çš„å·¥ä½œå° + label_my_account: 我的å¸å· + label_my_projects: 我的项目 + label_my_page_block: æˆ‘çš„å·¥ä½œå°æ¨¡å— + label_administration: ç®¡ç† + label_login: 登录 + label_logout: 退出 + label_help: 帮助 + label_reported_issues: 已报告的问题 + label_assigned_to_me_issues: 指派给我的问题 + label_last_login: 最åŽç™»å½• + label_registered_on: 注册于 + label_activity: 活动 + label_overall_activity: 活动概览 + label_user_activity: "%{value} 的活动" + label_new: 新建 + label_logged_as: 登录为 + label_environment: 环境 + label_authentication: è®¤è¯ + label_auth_source: è®¤è¯æ¨¡å¼ + label_auth_source_new: æ–°å»ºè®¤è¯æ¨¡å¼ + label_auth_source_plural: è®¤è¯æ¨¡å¼ + label_subproject_plural: å项目 + label_subproject_new: 新建å项目 + label_and_its_subprojects: "%{value} åŠå…¶å项目" + label_min_max_length: æœ€å° - 最大 长度 + label_list: 列表 + label_date: 日期 + label_integer: æ•´æ•° + label_float: 浮点数 + label_boolean: 布尔值 + label_string: å—符串 + label_text: 文本 + label_attribute: 属性 + label_attribute_plural: 属性 + label_no_data: 没有任何数æ®å¯ä¾›æ˜¾ç¤º + label_change_status: å˜æ›´çŠ¶æ€ + label_history: 历å²è®°å½• + label_attachment: 文件 + label_attachment_new: 新建文件 + label_attachment_delete: åˆ é™¤æ–‡ä»¶ + label_attachment_plural: 文件 + label_file_added: æ–‡ä»¶å·²æ·»åŠ + label_report: 报表 + label_report_plural: 报表 + label_news: æ–°é—» + label_news_new: æ·»åŠ æ–°é—» + label_news_plural: æ–°é—» + label_news_latest: 最近的新闻 + label_news_view_all: 查看所有新闻 + label_news_added: æ–°é—»å·²æ·»åŠ + label_settings: é…ç½® + label_overview: 概述 + label_version: 版本 + label_version_new: 新建版本 + label_version_plural: 版本 + label_close_versions: å…³é—已完æˆçš„版本 + label_confirmation: 确认 + label_export_to: 导出 + label_read: 读å–... + label_public_projects: 公开的项目 + label_open_issues: 打开 + label_open_issues_plural: 打开 + label_closed_issues: å·²å…³é— + label_closed_issues_plural: å·²å…³é— + label_x_open_issues_abbr_on_total: + zero: 0 打开 / %{total} + one: 1 打开 / %{total} + other: "%{count} 打开 / %{total}" + label_x_open_issues_abbr: + zero: 0 打开 + one: 1 打开 + other: "%{count} 打开" + label_x_closed_issues_abbr: + zero: 0 å·²å…³é— + one: 1 å·²å…³é— + other: "%{count} 已关é—" + label_total: åˆè®¡ + label_permissions: æƒé™ + label_current_status: 当å‰çŠ¶æ€ + label_new_statuses_allowed: å…è®¸çš„æ–°çŠ¶æ€ + label_all: 全部 + label_none: æ— + label_nobody: æ— äºº + label_next: 下一页 + label_previous: 上一页 + label_used_by: ä½¿ç”¨ä¸ + label_details: 详情 + label_add_note: æ·»åŠ è¯´æ˜Ž + label_per_page: æ¯é¡µ + label_calendar: 日历 + label_months_from: ä¸ªæœˆä»¥æ¥ + label_gantt: 甘特图 + label_internal: 内部 + label_last_changes: "最近的 %{count} æ¬¡å˜æ›´" + label_change_view_all: æŸ¥çœ‹æ‰€æœ‰å˜æ›´ + label_personalize_page: 个性化定制本页 + label_comment: 评论 + label_comment_plural: 评论 + label_x_comments: + zero: æ— è¯„è®º + one: 1 æ¡è¯„论 + other: "%{count} æ¡è¯„论" + label_comment_add: æ·»åŠ è¯„è®º + label_comment_added: è¯„è®ºå·²æ·»åŠ + label_comment_delete: åˆ é™¤è¯„è®º + label_query: 自定义查询 + label_query_plural: 自定义查询 + label_query_new: 新建查询 + label_filter_add: å¢žåŠ è¿‡æ»¤å™¨ + label_filter_plural: 过滤器 + label_equals: ç‰äºŽ + label_not_equals: ä¸ç‰äºŽ + label_in_less_than: 剩余天数å°äºŽ + label_in_more_than: 剩余天数大于 + label_greater_or_equal: '>=' + label_less_or_equal: '<=' + label_in: 剩余天数 + label_today: 今天 + label_all_time: 全部时间 + label_yesterday: 昨天 + label_this_week: 本周 + label_last_week: 上周 + label_last_n_days: "æœ€åŽ %{count} 天" + label_this_month: 本月 + label_last_month: 上月 + label_this_year: 今年 + label_date_range: 日期范围 + label_less_than_ago: 之å‰å¤©æ•°å°‘于 + label_more_than_ago: 之å‰å¤©æ•°å¤§äºŽ + label_ago: 之å‰å¤©æ•° + label_contains: åŒ…å« + label_not_contains: ä¸åŒ…å« + label_day_plural: 天 + label_repository: 版本库 + label_repository_plural: 版本库 + label_browse: æµè§ˆ + label_branch: 分支 + label_tag: æ ‡ç¾ + label_revision: 修订 + label_revision_plural: 修订 + label_revision_id: 修订 %{value} + label_associated_revisions: 相关修订版本 + label_added: å·²æ·»åŠ + label_modified: 已修改 + label_copied: å·²å¤åˆ¶ + label_renamed: å·²é‡å‘½å + label_deleted: å·²åˆ é™¤ + label_latest_revision: 最近的修订版本 + label_latest_revision_plural: 最近的修订版本 + label_view_revisions: 查看修订 + label_view_all_revisions: 查看所有修订 + label_max_size: 最大尺寸 + label_sort_highest: 置顶 + label_sort_higher: 上移 + label_sort_lower: 下移 + label_sort_lowest: 置底 + label_roadmap: 路线图 + label_roadmap_due_in: "æˆªæ¢æ—¥æœŸåˆ° %{value}" + label_roadmap_overdue: "%{value} 延期" + label_roadmap_no_issues: 该版本没有问题 + label_search: æœç´¢ + label_result_plural: 结果 + label_all_words: 所有å•è¯ + label_wiki: Wiki + label_wiki_edit: Wiki 编辑 + label_wiki_edit_plural: Wiki 编辑记录 + label_wiki_page: Wiki é¡µé¢ + label_wiki_page_plural: Wiki é¡µé¢ + label_index_by_title: æŒ‰æ ‡é¢˜ç´¢å¼• + label_index_by_date: 按日期索引 + label_current_version: 当å‰ç‰ˆæœ¬ + label_preview: 预览 + label_feed_plural: Feeds + label_changes_details: æ‰€æœ‰å˜æ›´çš„详情 + label_issue_tracking: 问题跟踪 + label_spent_time: 耗时 + label_overall_spent_time: 总体耗时 + label_f_hour: "%{value} å°æ—¶" + label_f_hour_plural: "%{value} å°æ—¶" + label_time_tracking: 时间跟踪 + label_change_plural: å˜æ›´ + label_statistics: 统计 + label_commits_per_month: æ¯æœˆæäº¤æ¬¡æ•° + label_commits_per_author: æ¯ç”¨æˆ·æäº¤æ¬¡æ•° + label_view_diff: 查看差别 + label_diff_inline: 直列 + label_diff_side_by_side: 并排 + label_options: 选项 + label_copy_workflow_from: 从以下选项å¤åˆ¶å·¥ä½œæµç¨‹ + label_permissions_report: æƒé™æŠ¥è¡¨ + label_watched_issues: 跟踪的问题 + label_related_issues: 相关的问题 + label_applied_status: 应用åŽçš„çŠ¶æ€ + label_loading: 载入ä¸... + label_relation_new: æ–°å»ºå…³è” + label_relation_delete: åˆ é™¤å…³è” + label_relates_to: å…³è”到 + label_duplicates: é‡å¤ + label_duplicated_by: 与其é‡å¤ + label_blocks: 阻挡 + label_blocked_by: 被阻挡 + label_precedes: 优先于 + label_follows: è·ŸéšäºŽ + label_end_to_start: 结æŸ-开始 + label_end_to_end: 结æŸ-ç»“æŸ + label_start_to_start: 开始-开始 + label_start_to_end: 开始-ç»“æŸ + label_stay_logged_in: ä¿æŒç™»å½•çŠ¶æ€ + label_disabled: ç¦ç”¨ + label_show_completed_versions: 显示已完æˆçš„版本 + label_me: 我 + label_board: 讨论区 + label_board_new: 新建讨论区 + label_board_plural: 讨论区 + label_board_locked: é”定 + label_board_sticky: 置顶 + label_topic_plural: 主题 + label_message_plural: 帖å + label_message_last: 最新的帖å + label_message_new: æ–°è´´ + label_message_posted: å‘帖æˆåŠŸ + label_reply_plural: å›žå¤ + label_send_information: 给用户å‘é€å¸å·ä¿¡æ¯ + label_year: å¹´ + label_month: 月 + label_week: 周 + label_date_from: 从 + label_date_to: 到 + label_language_based: æ ¹æ®ç”¨æˆ·çš„è¯è¨€ + label_sort_by: "æ ¹æ® %{value} 排åº" + label_send_test_email: å‘逿µ‹è¯•邮件 + label_feeds_access_key: Atomå˜å–é”® + label_missing_feeds_access_key: 缺少Atomå˜å–é”® + label_feeds_access_key_created_on: "Atomå˜å–键是在 %{value} 之å‰å»ºç«‹çš„" + label_module_plural: æ¨¡å— + label_added_time_by: "ç”± %{author} 在 %{age} 之剿·»åŠ " + label_updated_time: " 更新于 %{value} 之å‰" + label_updated_time_by: "ç”± %{author} 更新于 %{age} 之å‰" + label_jump_to_a_project: 选择一个项目... + label_file_plural: 文件 + label_changeset_plural: å˜æ›´ + label_default_columns: 默认列 + label_no_change_option: (ä¸å˜) + label_bulk_edit_selected_issues: 批é‡ä¿®æ”¹é€‰ä¸çš„问题 + label_theme: 主题 + label_default: 默认 + label_search_titles_only: ä»…åœ¨æ ‡é¢˜ä¸æœç´¢ + label_user_mail_option_all: "æ”¶å–æˆ‘的项目的所有通知" + label_user_mail_option_selected: "æ”¶å–选ä¸é¡¹ç›®çš„æ‰€æœ‰é€šçŸ¥..." + label_user_mail_option_none: "䏿”¶å–任何通知" + label_user_mail_option_only_my_events: "åªæ”¶å–我跟踪或å‚与的项目的通知" + label_user_mail_option_only_assigned: "åªæ”¶å–分é…给我的" + label_user_mail_option_only_owner: åªæ”¶å–由我创建的 + label_user_mail_no_self_notified: "ä¸è¦å‘é€å¯¹æˆ‘自己æäº¤çš„修改的通知" + label_registration_activation_by_email: é€šè¿‡é‚®ä»¶è®¤è¯æ¿€æ´»å¸å· + label_registration_manual_activation: 手动激活å¸å· + label_registration_automatic_activation: 自动激活å¸å· + label_display_per_page: "æ¯é¡µæ˜¾ç¤ºï¼š%{value}" + label_age: æäº¤æ—¶é—´ + label_change_properties: 修改属性 + label_general: 一般 + label_more: 更多 + label_scm: SCM + label_plugins: æ’ä»¶ + label_ldap_authentication: LDAP è®¤è¯ + label_downloads_abbr: D/L + label_optional_description: å¯é€‰çš„æè¿° + label_add_another_file: æ·»åŠ å…¶å®ƒæ–‡ä»¶ + label_preferences: 首选项 + label_chronological_order: æŒ‰æ—¶é—´é¡ºåº + label_reverse_chronological_order: 按时间顺åºï¼ˆå€’åºï¼‰ + label_planning: 计划 + label_incoming_emails: 接收邮件 + label_generate_key: 生æˆä¸€ä¸ªkey + label_issue_watchers: 跟踪者 + label_example: 示例 + label_display: 显示 + label_sort: æŽ’åº + label_ascending: å‡åº + label_descending: é™åº + label_date_from_to: 从 %{start} 到 %{end} + label_wiki_content_added: Wiki 页é¢å·²æ·»åŠ + label_wiki_content_updated: Wiki 页é¢å·²æ›´æ–° + label_group: 组 + label_group_plural: 组 + label_group_new: 新建组 + label_time_entry_plural: 耗时 + label_version_sharing_none: ä¸å…±äº« + label_version_sharing_descendants: 与å项目共享 + label_version_sharing_hierarchy: 与项目继承层次共享 + label_version_sharing_tree: ä¸Žé¡¹ç›®æ ‘å…±äº« + label_version_sharing_system: 与所有项目共享 + label_update_issue_done_ratios: 更新问题的完æˆåº¦ + label_copy_source: æº + label_copy_target: ç›®æ ‡ + label_copy_same_as_target: ä¸Žç›®æ ‡ä¸€è‡´ + label_display_used_statuses_only: åªæ˜¾ç¤ºè¢«æ¤è·Ÿè¸ªæ ‡ç¾ä½¿ç”¨çš„çŠ¶æ€ + label_api_access_key: API访问键 + label_missing_api_access_key: 缺少API访问键 + label_api_access_key_created_on: API访问键是在 %{value} 之å‰å»ºç«‹çš„ + label_profile: 简介 + label_subtask_plural: å任务 + label_project_copy_notifications: å¤åˆ¶é¡¹ç›®æ—¶å‘é€é‚®ä»¶é€šçŸ¥ + label_principal_search: "æœç´¢ç”¨æˆ·æˆ–组:" + label_user_search: "æœç´¢ç”¨æˆ·ï¼š" + + button_login: 登录 + button_submit: æäº¤ + button_save: ä¿å˜ + button_check_all: 全选 + button_uncheck_all: 清除 + button_delete: åˆ é™¤ + button_create: 创建 + button_create_and_continue: åˆ›å»ºå¹¶ç»§ç» + button_test: 测试 + button_edit: 编辑 + button_edit_associated_wikipage: "编辑相关wiki页é¢: %{page_title}" + button_add: 新增 + button_change: 修改 + button_apply: 应用 + button_clear: 清除 + button_lock: é”定 + button_unlock: è§£é” + button_download: 下载 + button_list: 列表 + button_view: 查看 + button_move: 移动 + button_move_and_follow: 移动并转到新问题 + button_back: 返回 + button_cancel: å–æ¶ˆ + button_activate: 激活 + button_sort: æŽ’åº + button_log_time: 登记工时 + button_rollback: æ¢å¤åˆ°è¿™ä¸ªç‰ˆæœ¬ + button_watch: 跟踪 + button_unwatch: å–æ¶ˆè·Ÿè¸ª + button_reply: å›žå¤ + button_archive: å˜æ¡£ + button_unarchive: å–æ¶ˆå˜æ¡£ + button_reset: é‡ç½® + button_rename: é‡å‘½å/é‡å®šå‘ + button_change_password: 修改密ç + button_copy: å¤åˆ¶ + button_copy_and_follow: å¤åˆ¶å¹¶è½¬åˆ°æ–°é—®é¢˜ + button_annotate: 追溯 + button_update: æ›´æ–° + button_configure: é…ç½® + button_quote: 引用 + button_duplicate: 副本 + button_show: 显示 + + status_active: 活动的 + status_registered: 已注册 + status_locked: å·²é”定 + + version_status_open: 打开 + version_status_locked: é”定 + version_status_closed: å…³é— + + field_active: 活动 + + text_select_mail_notifications: 选择需è¦å‘é€é‚®ä»¶é€šçŸ¥çš„动作 + text_regexp_info: 例如:^[A-Z0-9]+$ + text_min_max_length_info: 0 表示没有é™åˆ¶ + text_project_destroy_confirmation: 您确信è¦åˆ é™¤è¿™ä¸ªé¡¹ç›®ä»¥åŠæ‰€æœ‰ç›¸å…³çš„æ•°æ®å—? + text_subprojects_destroy_warning: "以下åé¡¹ç›®ä¹Ÿå°†è¢«åŒæ—¶åˆ 除:%{value}" + text_workflow_edit: é€‰æ‹©è§’è‰²å’Œè·Ÿè¸ªæ ‡ç¾æ¥ç¼–辑工作æµç¨‹ + text_are_you_sure: 您确定? + text_journal_changed: "%{label} 从 %{old} å˜æ›´ä¸º %{new}" + text_journal_set_to: "%{label} 被设置为 %{value}" + text_journal_deleted: "%{label} å·²åˆ é™¤ (%{old})" + text_journal_added: "%{label} %{value} å·²æ·»åŠ " + text_tip_issue_begin_day: 今天开始的任务 + text_tip_issue_end_day: 今天结æŸçš„任务 + text_tip_issue_begin_end_day: 今天开始并结æŸçš„任务 + text_caracters_maximum: "最多 %{count} 个å—符。" + text_caracters_minimum: "è‡³å°‘éœ€è¦ %{count} 个å—符。" + text_length_between: "长度必须在 %{min} 到 %{max} 个å—符之间。" + text_tracker_no_workflow: æ¤è·Ÿè¸ªæ ‡ç¾æœªå®šä¹‰å·¥ä½œæµç¨‹ + text_unallowed_characters: éžæ³•å—符 + text_comma_separated: å¯ä»¥ä½¿ç”¨å¤šä¸ªå€¼ï¼ˆç”¨é€—å·,分开)。 + text_line_separated: å¯ä»¥ä½¿ç”¨å¤šä¸ªå€¼ï¼ˆæ¯è¡Œä¸€ä¸ªå€¼ï¼‰ã€‚ + text_issues_ref_in_commit_messages: 在æäº¤ä¿¡æ¯ä¸å¼•用和解决问题 + text_issue_added: "问题 %{id} 已由 %{author} æäº¤ã€‚" + text_issue_updated: "问题 %{id} 已由 %{author} 更新。" + text_wiki_destroy_confirmation: 您确定è¦åˆ 除这个 wiki åŠå…¶æ‰€æœ‰å†…容å—? + text_issue_category_destroy_question: "有一些问题(%{count} 个)属于æ¤ç±»åˆ«ã€‚æ‚¨æƒ³è¿›è¡Œå“ªç§æ“作?" + text_issue_category_destroy_assignments: åˆ é™¤é—®é¢˜çš„æ‰€å±žç±»åˆ«ï¼ˆé—®é¢˜å˜ä¸ºæ— 类别) + text_issue_category_reassign_to: 为问题选择其它类别 + text_user_mail_option: "对于没有选ä¸çš„项目,您将åªä¼šæ”¶åˆ°æ‚¨è·Ÿè¸ªæˆ–å‚与的项目的通知(比如说,您是问题的报告者, 或被指派解决æ¤é—®é¢˜ï¼‰ã€‚" + text_no_configuration_data: "角色ã€è·Ÿè¸ªæ ‡ç¾ã€é—®é¢˜çжæ€å’Œå·¥ä½œæµç¨‹è¿˜æ²¡æœ‰è®¾ç½®ã€‚\n强烈建议您先载入默认设置,然åŽåœ¨æ¤åŸºç¡€ä¸Šè¿›è¡Œä¿®æ”¹ã€‚" + text_load_default_configuration: 载入默认设置 + text_status_changed_by_changeset: "å·²åº”ç”¨åˆ°å˜æ›´åˆ—表 %{value}." + text_time_logged_by_changeset: "已应用到修订版本 %{value}." + text_issues_destroy_confirmation: '您确定è¦åˆ 除选ä¸çš„问题å—?' + text_select_project_modules: '请选择æ¤é¡¹ç›®å¯ä»¥ä½¿ç”¨çš„æ¨¡å—:' + text_default_administrator_account_changed: 默认的管ç†å‘˜å¸å·å·²æ”¹å˜ + text_file_repository_writable: 附件路径å¯å†™ + text_plugin_assets_writable: æ’件的附件路径å¯å†™ + text_rmagick_available: RMagick å¯ç”¨ï¼ˆå¯é€‰çš„) + text_destroy_time_entries_question: 您è¦åˆ 除的问题已ç»ä¸ŠæŠ¥äº† %{hours} å°æ—¶çš„工作é‡ã€‚æ‚¨æƒ³è¿›è¡Œé‚£ç§æ“作? + text_destroy_time_entries: åˆ é™¤ä¸ŠæŠ¥çš„å·¥ä½œé‡ + text_assign_time_entries_to_project: å°†å·²ä¸ŠæŠ¥çš„å·¥ä½œé‡æäº¤åˆ°é¡¹ç›®ä¸ + text_reassign_time_entries: 'å°†å·²ä¸ŠæŠ¥çš„å·¥ä½œé‡æŒ‡å®šåˆ°æ¤é—®é¢˜ï¼š' + text_user_wrote: "%{value} 写到:" + text_enumeration_destroy_question: "%{count} 个对象被关è”到了这个枚举值。" + text_enumeration_category_reassign_to: '将它们关è”到新的枚举值:' + text_email_delivery_not_configured: "邮件傿•°å°šæœªé…ç½®ï¼Œå› æ¤é‚®ä»¶é€šçŸ¥åŠŸèƒ½å·²è¢«ç¦ç”¨ã€‚\n请在config/configuration.ymlä¸é…置您的SMTPæœåŠ¡å™¨ä¿¡æ¯å¹¶é‡æ–°å¯åŠ¨ä»¥ä½¿å…¶ç”Ÿæ•ˆã€‚" + text_repository_usernames_mapping: "选择或更新与版本库ä¸çš„用户å对应的Redmine用户。\n版本库ä¸ä¸ŽRedmineä¸çš„åŒå用户将被自动对应。" + text_diff_truncated: '... å·®åˆ«å†…å®¹è¶…è¿‡äº†å¯æ˜¾ç¤ºçš„æœ€å¤§è¡Œæ•°å¹¶å·²è¢«æˆªæ–' + text_custom_field_possible_values_info: 'æ¯é¡¹æ•°å€¼ä¸€è¡Œ' + text_wiki_page_destroy_question: æ¤é¡µé¢æœ‰ %{descendants} 个å页é¢å’Œä¸‹çº§é¡µé¢ã€‚æ‚¨æƒ³è¿›è¡Œé‚£ç§æ“作? + text_wiki_page_nullify_children: å°†å页é¢ä¿ç•™ä¸ºæ ¹é¡µé¢ + text_wiki_page_destroy_children: åˆ é™¤å页é¢åŠå…¶æ‰€æœ‰ä¸‹çº§é¡µé¢ + text_wiki_page_reassign_children: å°†å页é¢çš„上级页é¢è®¾ç½®ä¸º + text_own_membership_delete_confirmation: ä½ æ£åœ¨åˆ é™¤ä½ çŽ°æœ‰çš„æŸäº›æˆ–全部æƒé™ï¼Œå¦‚æžœè¿™æ ·åšäº†ä½ å¯èƒ½å°†ä¼šå†ä¹Ÿæ— æ³•ç¼–è¾‘è¯¥é¡¹ç›®äº†ã€‚ä½ ç¡®å®šè¦ç»§ç»å—? + text_zoom_in: 放大 + text_zoom_out: ç¼©å° + + default_role_manager: 管ç†äººå‘˜ + default_role_developer: å¼€å‘人员 + default_role_reporter: 报告人员 + default_tracker_bug: 错误 + default_tracker_feature: 功能 + default_tracker_support: æ”¯æŒ + default_issue_status_new: 新建 + default_issue_status_in_progress: è¿›è¡Œä¸ + default_issue_status_resolved: 已解决 + default_issue_status_feedback: å馈 + default_issue_status_closed: å·²å…³é— + default_issue_status_rejected: å·²æ‹’ç» + default_doc_category_user: 用户文档 + default_doc_category_tech: 技术文档 + default_priority_low: 低 + default_priority_normal: 普通 + default_priority_high: 高 + default_priority_urgent: 紧急 + default_priority_immediate: 立刻 + default_activity_design: 设计 + default_activity_development: å¼€å‘ + + enumeration_issue_priorities: 问题优先级 + enumeration_doc_categories: 文档类别 + enumeration_activities: 活动(时间跟踪) + enumeration_system_activity: 系统活动 + + field_warn_on_leaving_unsaved: 当离开未ä¿å˜å†…å®¹çš„é¡µé¢æ—¶ï¼Œæç¤ºæˆ‘ + text_warn_on_leaving_unsaved: 若离开当å‰é¡µé¢ï¼Œåˆ™è¯¥é¡µé¢å†…未ä¿å˜çš„内容将丢失。 + label_my_queries: 我的自定义查询 + text_journal_changed_no_detail: "%{label} 已更新。" + label_news_comment_added: æ·»åŠ åˆ°æ–°é—»çš„è¯„è®º + button_expand_all: 展开所有 + button_collapse_all: åˆæ‹¢æ‰€æœ‰ + label_additional_workflow_transitions_for_assignee: 当用户是问题的分é…对象时所å…许的问题状æ€è½¬æ¢ + label_additional_workflow_transitions_for_author: 当用户是问题作者时所å…许的问题状æ€è½¬æ¢ + label_bulk_edit_selected_time_entries: 批é‡ä¿®æ”¹é€‰å®šçš„æ—¶é—´æ¡ç›® + text_time_entries_destroy_confirmation: 是å¦ç¡®å®šè¦åˆ 除选定的时间æ¡ç›®ï¼Ÿ + label_role_anonymous: Anonymous + label_role_non_member: Non member + label_issue_note_added: é—®é¢˜å¤‡æ³¨å·²æ·»åŠ + label_issue_status_updated: é—®é¢˜çŠ¶æ€æ›´æ–° + label_issue_priority_updated: 问题优先级更新 + label_issues_visibility_own: 创建或分é…给用户的问题 + field_issues_visibility: 问题å¯è§ + label_issues_visibility_all: 全部问题 + permission_set_own_issues_private: è®¾ç½®è‡ªå·±çš„é—®é¢˜ä¸ºå…¬å¼€æˆ–ç§æœ‰ + field_is_private: ç§æœ‰ + permission_set_issues_private: è®¾ç½®é—®é¢˜ä¸ºå…¬å¼€æˆ–ç§æœ‰ + label_issues_visibility_public: 全部éžç§æœ‰é—®é¢˜ + text_issues_destroy_descendants_confirmation: æ¤æ“ä½œåŒæ—¶ä¼šåˆ 除 %{count} 个å任务。 + + field_commit_logs_encoding: æäº¤æ³¨é‡Šçš„ç¼–ç + field_scm_path_encoding: 路径编ç + text_scm_path_encoding_note: "默认: UTF-8" + field_path_to_repository: 库路径 + field_root_directory: æ ¹ç›®å½• + field_cvs_module: CVS Module + field_cvsroot: CVSROOT + text_mercurial_repository_note: 本地库 (e.g. /hgrepo, c:\hgrepo) + text_scm_command: 命令 + text_scm_command_version: 版本 + label_git_report_last_commit: 报告最åŽä¸€æ¬¡æ–‡ä»¶/目录æäº¤ + text_scm_config: 您å¯ä»¥åœ¨config/configuration.ymlä¸é…置您的SCM命令。 请在编辑åŽï¼Œé‡å¯Redmine应用。 + text_scm_command_not_available: Scm命令ä¸å¯ç”¨ã€‚ 请检查管ç†é¢æ¿çš„é…置。 + text_git_repository_note: åº“ä¸æ— 内容。(e.g. /gitrepo, c:\gitrepo) + notice_issue_successful_create: 问题 %{id} 已创建。 + label_between: 介于 + setting_issue_group_assignment: å…许问题被分é…给组 + label_diff: diff + description_query_sort_criteria_direction: æŽ’åºæ–¹å¼ + description_project_scope: æœç´¢èŒƒå›´ + description_filter: 过滤器 + description_user_mail_notification: 邮件通知设置 + description_date_from: 输入开始日期 + description_message_content: ä¿¡æ¯å†…容 + description_available_columns: 备选列 + description_date_range_interval: æŒ‰å¼€å§‹æ—¥æœŸå’Œç»“æŸæ—¥æœŸé€‰æ‹©èŒƒå›´ + description_issue_category_reassign: 选择问题类别 + description_search: æœç´¢å—段 + description_notes: 批注 + description_date_range_list: 从列表ä¸é€‰æ‹©èŒƒå›´ + description_choose_project: 项目 + description_date_to: è¾“å…¥ç»“æŸæ—¥æœŸ + description_query_sort_criteria_attribute: æŽ’åºæ–¹å¼ + description_wiki_subpages_reassign: é€‰æ‹©çˆ¶é¡µé¢ + description_selected_columns: 已选列 + label_parent_revision: 父修订 + label_child_revision: å修订 + error_scm_annotate_big_text_file: è¾“å…¥æ–‡æœ¬å†…å®¹è¶…é•¿ï¼Œæ— æ³•è¾“å…¥ã€‚ + setting_default_issue_start_date_to_creation_date: ä½¿ç”¨å½“å‰æ—¥æœŸä½œä¸ºæ–°é—®é¢˜çš„开始日期 + button_edit_section: 编辑æ¤åŒºåŸŸ + setting_repositories_encodings: 附件和版本库编ç + description_all_columns: 所有列 + button_export: 导出 + label_export_options: "%{export_format} 导出选项" + error_attachment_too_big: è¯¥æ–‡ä»¶æ— æ³•ä¸Šä¼ ã€‚è¶…è¿‡æ–‡ä»¶å¤§å°é™åˆ¶ (%{max_size}) + notice_failed_to_save_time_entries: "æ— æ³•ä¿å˜ä¸‹åˆ—所选å–çš„ %{total} 个项目ä¸çš„ %{count} 工时: %{ids}。" + label_x_issues: + zero: 0 问题 + one: 1 问题 + other: "%{count} 问题" + label_repository_new: 新建版本库 + field_repository_is_default: 主版本库 + label_copy_attachments: å¤åˆ¶é™„ä»¶ + label_item_position: "%{position}/%{count}" + label_completed_versions: 已完æˆçš„版本 + text_project_identifier_info: ä»…å°å†™å—æ¯ï¼ˆa-zï¼‰ã€æ•°å—ã€ç ´æŠ˜å·ï¼ˆ-)和下划线(_)å¯ä»¥ä½¿ç”¨ã€‚text
', + 'p{color:red;}. text' => 'text
', + 'p{color: red}. text' => 'text
', + 'p{color:#f00}. text' => 'text
', + 'p{color:#ff0000}. text' => 'text
', + 'p{border:10px}. text' => 'text
', + 'p{border:10}. text' => 'text
', + 'p{border:10%}. text' => 'text
', + 'p{border:10em}. text' => 'text
', + 'p{border:1.5em}. text' => 'text
', + 'p{border-left:1px}. text' => 'text
', + 'p{border-right:1px}. text' => 'text
', + 'p{border-top:1px}. text' => 'text
', + 'p{border-bottom:1px}. text' => 'text
', + }, false) + + # multiple styles + assert_html_output({ + 'p{color:red; border-top:1px}. text' => 'text
', + 'p{color:red ; border-top:1px}. text' => 'text
', + 'p{color:red;border-top:1px}. text' => 'text
', + }, false) + + # styles with multiple values + assert_html_output({ + 'p{border:1px solid red;}. text' => 'text
', + 'p{border-top-left-radius: 10px 5px;}. text' => 'text
', + }, false) + end + + def test_invalid_styles_should_be_filtered + assert_html_output({ + 'p{invalid}. text' => 'text
', + 'p{invalid:red}. text' => 'text
', + 'p{color:(red)}. text' => 'text
', + 'p{color:red;invalid:blue}. text' => 'text
', + 'p{invalid:blue;color:red}. text' => 'text
', + 'p{color:"}. text' => 'p{color:"}. text
', + }, false) + end + + def test_inline_code + assert_html_output( + 'this is @some code@' => 'this issome code
',
+ '@<Location /redmine>
'
+ )
+ end
+
+ def test_nested_lists
+ raw = <<-RAW
+# Item 1
+# Item 2
+** Item 2a
+** Item 2b
+# Item 3
+** Item 3a
+RAW
+
+ expected = <<-EXPECTED
+<script>some script;</script>
", + # do not escape pre/code tags + "\nline 1\nline2" => "
\nline 1\nline2", + "
\nline 1\nline2
" => "\nline 1\nline2
",
+ "" => "content
<div>content</div>", + "HTML comment: " => "
HTML comment: <!-- no comments -->
", + " +<%= yield :header_tags -%> + + +